diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8072c715..810797e4 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -25,7 +25,7 @@ - + \ No newline at end of file diff --git a/res/layout/activity_advanced_map_viewer.xml b/res/layout/activity_advanced_map_viewer.xml index 8ec32a07..842a59e8 100644 --- a/res/layout/activity_advanced_map_viewer.xml +++ b/res/layout/activity_advanced_map_viewer.xml @@ -4,7 +4,7 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" > - diff --git a/src/de/sfb/tilemap/InfoView.java b/src/de/sfb/tilemap/InfoView.java index 091b0385..e31e4ad4 100644 --- a/src/de/sfb/tilemap/InfoView.java +++ b/src/de/sfb/tilemap/InfoView.java @@ -19,7 +19,6 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.view.WindowManager; import android.webkit.WebView; -import de.sfb.tilemap.R; /** * Simple activity to display the info web page from the assets folder. diff --git a/src/de/sfb/tilemap/MyLocationListener.java b/src/de/sfb/tilemap/MyLocationListener.java index 1b88c47f..4dbca49f 100644 --- a/src/de/sfb/tilemap/MyLocationListener.java +++ b/src/de/sfb/tilemap/MyLocationListener.java @@ -19,7 +19,6 @@ import org.mapsforge.core.GeoPoint; import android.location.Location; import android.location.LocationListener; import android.os.Bundle; -import de.sfb.tilemap.R; class MyLocationListener implements LocationListener { private final TileMap advancedMapViewer; diff --git a/src/de/sfb/tilemap/SeekBarChangeListener.java b/src/de/sfb/tilemap/SeekBarChangeListener.java index 6677476a..4583ed6f 100644 --- a/src/de/sfb/tilemap/SeekBarChangeListener.java +++ b/src/de/sfb/tilemap/SeekBarChangeListener.java @@ -16,7 +16,6 @@ package de.sfb.tilemap; import android.widget.SeekBar; import android.widget.TextView; -import de.sfb.tilemap.R; class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener { private final TextView textView; diff --git a/src/de/sfb/tilemap/TileMap.java b/src/de/sfb/tilemap/TileMap.java index e6ccfc8c..83292ffc 100755 --- a/src/de/sfb/tilemap/TileMap.java +++ b/src/de/sfb/tilemap/TileMap.java @@ -5,20 +5,20 @@ import java.io.FileNotFoundException; import java.text.DateFormat; import java.util.Date; -import org.mapsforge.android.maps.DebugSettings; -import org.mapsforge.android.maps.MapActivity; -import org.mapsforge.android.maps.MapController; -import org.mapsforge.android.maps.MapScaleBar; -import org.mapsforge.android.maps.MapView; -import org.mapsforge.android.maps.mapgenerator.MapDatabaseFactory; -import org.mapsforge.android.maps.mapgenerator.MapDatabaseInternal; -import org.mapsforge.android.maps.mapgenerator.MapGenerator; -import org.mapsforge.android.maps.rendertheme.InternalRenderTheme; -import org.mapsforge.android.maps.utils.AndroidUtils; +import org.mapsforge.android.DebugSettings; +import org.mapsforge.android.MapActivity; +import org.mapsforge.android.MapController; +import org.mapsforge.android.MapScaleBar; +import org.mapsforge.android.MapView; +import org.mapsforge.android.mapgenerator.MapDatabaseFactory; +import org.mapsforge.android.mapgenerator.MapDatabaseInternal; +import org.mapsforge.android.mapgenerator.MapGenerator; +import org.mapsforge.android.rendertheme.InternalRenderTheme; +import org.mapsforge.android.utils.AndroidUtils; import org.mapsforge.core.BoundingBox; import org.mapsforge.core.GeoPoint; -import org.mapsforge.map.IMapDatabase; -import org.mapsforge.map.MapFileInfo; +import org.mapsforge.mapdatabase.IMapDatabase; +import org.mapsforge.mapdatabase.MapFileInfo; import android.app.AlertDialog; import android.app.Dialog; diff --git a/src/de/sfb/tilemap/filefilter/ValidFileFilter.java b/src/de/sfb/tilemap/filefilter/ValidFileFilter.java index 99a4dac8..66736bd7 100644 --- a/src/de/sfb/tilemap/filefilter/ValidFileFilter.java +++ b/src/de/sfb/tilemap/filefilter/ValidFileFilter.java @@ -16,7 +16,7 @@ package de.sfb.tilemap.filefilter; import java.io.FileFilter; -import org.mapsforge.map.FileOpenResult; +import org.mapsforge.mapdatabase.FileOpenResult; /** * An extension of the {@link FileFilter} interface. diff --git a/src/de/sfb/tilemap/filefilter/ValidMapFile.java b/src/de/sfb/tilemap/filefilter/ValidMapFile.java index d1341499..4f44566a 100644 --- a/src/de/sfb/tilemap/filefilter/ValidMapFile.java +++ b/src/de/sfb/tilemap/filefilter/ValidMapFile.java @@ -16,9 +16,9 @@ package de.sfb.tilemap.filefilter; import java.io.File; -import org.mapsforge.map.FileOpenResult; -import org.mapsforge.map.IMapDatabase; -import org.mapsforge.map.reader.MapDatabase; +import org.mapsforge.mapdatabase.FileOpenResult; +import org.mapsforge.mapdatabase.IMapDatabase; +import org.mapsforge.mapdatabase.mapfile.MapDatabase; /** * Accepts all valid map files. diff --git a/src/de/sfb/tilemap/filefilter/ValidRenderTheme.java b/src/de/sfb/tilemap/filefilter/ValidRenderTheme.java index a70fc92e..cbf410e9 100644 --- a/src/de/sfb/tilemap/filefilter/ValidRenderTheme.java +++ b/src/de/sfb/tilemap/filefilter/ValidRenderTheme.java @@ -22,8 +22,8 @@ import java.io.InputStream; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; -import org.mapsforge.android.maps.rendertheme.RenderThemeHandler; -import org.mapsforge.map.FileOpenResult; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.mapdatabase.FileOpenResult; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; diff --git a/src/org/mapsforge/android/DebugSettings.java b/src/org/mapsforge/android/DebugSettings.java new file mode 100644 index 00000000..11bf883a --- /dev/null +++ b/src/org/mapsforge/android/DebugSettings.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import java.io.Serializable; + +/** + * A simple DTO to stores flags for debugging rendered map tiles. + */ +public class DebugSettings implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * True if drawing of tile coordinates is enabled, false otherwise. + */ + public final boolean mDrawTileCoordinates; + + /** + * True if drawing of tile frames is enabled, false otherwise. + */ + public final boolean mDrawTileFrames; + + /** + * True if highlighting of water tiles is enabled, false otherwise. + */ + public final boolean mDisablePolygons; + + private final int mHashCodeValue; + + /** + * @param drawTileCoordinates + * if drawing of tile coordinates is enabled. + * @param drawTileFrames + * if drawing of tile frames is enabled. + * @param disablePolygons + * if highlighting of water tiles is enabled. + */ + public DebugSettings(boolean drawTileCoordinates, boolean drawTileFrames, boolean disablePolygons) { + mDrawTileCoordinates = drawTileCoordinates; + mDrawTileFrames = drawTileFrames; + mDisablePolygons = disablePolygons; + mHashCodeValue = calculateHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DebugSettings)) { + return false; + } + DebugSettings other = (DebugSettings) obj; + if (mDrawTileCoordinates != other.mDrawTileCoordinates) { + return false; + } + if (mDrawTileFrames != other.mDrawTileFrames) { + return false; + } + if (mDisablePolygons != other.mDisablePolygons) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return mHashCodeValue; + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 1; + result = 31 * result + (mDrawTileCoordinates ? 1231 : 1237); + result = 31 * result + (mDrawTileFrames ? 1231 : 1237); + result = 31 * result + (mDisablePolygons ? 1231 : 1237); + return result; + } +} diff --git a/src/org/mapsforge/android/MapActivity.java b/src/org/mapsforge/android/MapActivity.java new file mode 100644 index 00000000..5e367267 --- /dev/null +++ b/src/org/mapsforge/android/MapActivity.java @@ -0,0 +1,138 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import org.mapsforge.android.mapgenerator.MapGenerator; +import org.mapsforge.core.GeoPoint; +import org.mapsforge.core.MapPosition; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; + +/** + * MapActivity is the abstract base class which must be extended in order to use a {@link MapView}. There are no + * abstract methods in this implementation which subclasses need to override and no API key or registration is required. + *

+ * A subclass may create a MapView either via one of the MapView constructors or by inflating an XML layout file. It is + * possible to use more than one MapView at the same time. + *

+ * When the MapActivity is shut down, the current center position, zoom level and map file of the MapView are saved in a + * preferences file and restored in the next startup process. + */ +public abstract class MapActivity extends Activity { + private static final String KEY_LATITUDE = "latitude"; + private static final String KEY_LONGITUDE = "longitude"; + private static final String KEY_MAP_FILE = "mapFile"; + private static final String KEY_ZOOM_LEVEL = "zoomLevel"; + private static final String PREFERENCES_FILE = "MapActivity"; + + private static boolean containsMapViewPosition(SharedPreferences sharedPreferences) { + return sharedPreferences.contains(KEY_LATITUDE) && sharedPreferences.contains(KEY_LONGITUDE) + && sharedPreferences.contains(KEY_ZOOM_LEVEL); + } + + /** + * Counter to store the last ID given to a MapView. + */ + private int lastMapViewId; + + /** + * Internal list which contains references to all running MapView objects. + */ + private MapView mMapView; + + private void destroyMapViews() { + mMapView.destroy(); + } + + private void restoreMapView(MapView mapView) { + SharedPreferences sharedPreferences = getSharedPreferences(PREFERENCES_FILE, MODE_PRIVATE); + if (containsMapViewPosition(sharedPreferences)) { + MapGenerator mapGenerator = mapView.getMapGenerator(); + if (!mapGenerator.requiresInternetConnection() && sharedPreferences.contains(KEY_MAP_FILE)) { + // get and set the map file + mapView.setMapFile(sharedPreferences.getString(KEY_MAP_FILE, null)); + } + + // get and set the map position and zoom level + int latitudeE6 = sharedPreferences.getInt(KEY_LATITUDE, 0); + int longitudeE6 = sharedPreferences.getInt(KEY_LONGITUDE, 0); + int zoomLevel = sharedPreferences.getInt(KEY_ZOOM_LEVEL, -1); + + GeoPoint geoPoint = new GeoPoint(latitudeE6, longitudeE6); + MapPosition mapPosition = new MapPosition(geoPoint, (byte) zoomLevel, 1); + mapView.setCenterAndZoom(mapPosition); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + destroyMapViews(); + } + + @Override + protected void onPause() { + super.onPause(); + mMapView.onPause(); + + Editor editor = getSharedPreferences(PREFERENCES_FILE, MODE_PRIVATE).edit(); + editor.clear(); + + // save the map position and zoom level + MapPosition mapPosition = mMapView.getMapPosition().getMapPosition(); + if (mapPosition != null) { + GeoPoint geoPoint = mapPosition.geoPoint; + editor.putInt(KEY_LATITUDE, geoPoint.latitudeE6); + editor.putInt(KEY_LONGITUDE, geoPoint.longitudeE6); + editor.putInt(KEY_ZOOM_LEVEL, mapPosition.zoomLevel); + } + + if (!mMapView.getMapGenerator().requiresInternetConnection() && mMapView.getMapFile() != null) { + // save the map file + editor.putString(KEY_MAP_FILE, mMapView.getMapFile()); + } + + editor.commit(); + } + + @Override + protected void onResume() { + super.onResume(); + mMapView.onResume(); + } + + /** + * @return a unique MapView ID on each call. + */ + final int getMapViewId() { + return ++lastMapViewId; + } + + /** + * This method is called once by each MapView during its setup process. + * + * @param mapView + * the calling MapView. + */ + final void registerMapView(MapView mapView) { + if (mMapView != null) + return; + + mMapView = mapView; + restoreMapView(mapView); + } +} diff --git a/src/org/mapsforge/android/MapController.java b/src/org/mapsforge/android/MapController.java new file mode 100644 index 00000000..716c7a08 --- /dev/null +++ b/src/org/mapsforge/android/MapController.java @@ -0,0 +1,89 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import org.mapsforge.core.GeoPoint; + +import android.view.KeyEvent; +import android.view.View; + +/** + * A MapController is used to programmatically modify the position and zoom level of a MapView. Each MapController is + * assigned to a single MapView instance. To retrieve a MapController for a given MapView, use the + * {@link MapView#getController()} method. + */ +public final class MapController implements View.OnKeyListener { + private final MapView mMapView; + + /** + * @param mapView + * the MapView which should be controlled by this MapController. + */ + MapController(MapView mapView) { + mMapView = mapView; + } + + @Override + public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + // forward the event to the MapView + return mMapView.onKeyDown(keyCode, keyEvent); + } else if (keyEvent.getAction() == KeyEvent.ACTION_UP) { + // forward the event to the MapView + return mMapView.onKeyUp(keyCode, keyEvent); + } + return false; + } + + /** + * Sets the center of the MapView without an animation to the given point. + * + * @param geoPoint + * the new center point of the map. + */ + public void setCenter(GeoPoint geoPoint) { + mMapView.setCenter(geoPoint); + } + + /** + * Sets the zoom level of the MapView. + * + * @param zoomLevel + * the new zoom level, will be limited by the maximum and minimum possible zoom level. + * @return the new zoom level. + */ + public int setZoom(int zoomLevel) { + mMapView.zoom((byte) (zoomLevel - mMapView.getMapPosition().getZoomLevel())); + return mMapView.getMapPosition().getZoomLevel(); + } + + /** + * Increases the zoom level of the MapView, unless the maximum zoom level has been reached. + * + * @return true if the zoom level has been changed, false otherwise. + */ + public boolean zoomIn() { + return mMapView.zoom((byte) 1); + } + + /** + * Decreases the zoom level of the MapView, unless the minimum zoom level has been reached. + * + * @return true if the zoom level has been changed, false otherwise. + */ + public boolean zoomOut() { + return mMapView.zoom((byte) -1); + } +} diff --git a/src/org/mapsforge/android/MapRenderer.java b/src/org/mapsforge/android/MapRenderer.java new file mode 100644 index 00000000..d84e2933 --- /dev/null +++ b/src/org/mapsforge/android/MapRenderer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import org.mapsforge.android.mapgenerator.MapGeneratorJob; + +import android.opengl.GLSurfaceView; + +/** + * + */ +public interface MapRenderer extends GLSurfaceView.Renderer { + + /** + * @param mapGeneratorJob + * the mapGeneratorJob holding Tile data + * @return true if the tile was processed + */ + public boolean passTile(MapGeneratorJob mapGeneratorJob); + + /** + * @return true when tile passed to renderer is processed false otherwise. + * used to lock overwriting resources passed with the tile + * (e.g. lock until bitmap is loaded to texture) + */ + public boolean processedTile(); + + /** + * called by MapView on position and map changes + * + * @param clear + * ... + */ + public void redrawTiles(boolean clear); +} diff --git a/src/org/mapsforge/android/MapScaleBar.java b/src/org/mapsforge/android/MapScaleBar.java new file mode 100644 index 00000000..15f71010 --- /dev/null +++ b/src/org/mapsforge/android/MapScaleBar.java @@ -0,0 +1,273 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import java.util.HashMap; +import java.util.Map; + +import org.mapsforge.core.MapPosition; +import org.mapsforge.core.MercatorProjection; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; + +/** + * A MapScaleBar displays the ratio of a distance on the map to the corresponding distance on the ground. + */ +public class MapScaleBar { + /** + * Enumeration of all text fields. + */ + public enum TextField { + /** + * Unit symbol for one foot. + */ + FOOT, + + /** + * Unit symbol for one kilometer. + */ + KILOMETER, + + /** + * Unit symbol for one meter. + */ + METER, + + /** + * Unit symbol for one mile. + */ + MILE; + } + + private static final int BITMAP_HEIGHT = 50; + private static final int BITMAP_WIDTH = 150; + private static final double LATITUDE_REDRAW_THRESHOLD = 0.2; + private static final int MARGIN_BOTTOM = 5; + private static final int MARGIN_LEFT = 5; + private static final double METER_FOOT_RATIO = 0.3048; + private static final int ONE_KILOMETER = 1000; + private static final int ONE_MILE = 5280; + private static final Paint SCALE_BAR = new Paint(Paint.ANTI_ALIAS_FLAG); + private static final Paint SCALE_BAR_STROKE = new Paint(Paint.ANTI_ALIAS_FLAG); + private static final int[] SCALE_BAR_VALUES_IMPERIAL = { 26400000, 10560000, 5280000, 2640000, 1056000, 528000, + 264000, 105600, 52800, 26400, 10560, 5280, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1 }; + private static final int[] SCALE_BAR_VALUES_METRIC = { 10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000, + 50000, 20000, 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1 }; + private static final Paint SCALE_TEXT = new Paint(Paint.ANTI_ALIAS_FLAG); + private static final Paint SCALE_TEXT_STROKE = new Paint(Paint.ANTI_ALIAS_FLAG); + + private static void configurePaints() { + SCALE_BAR.setStrokeWidth(2); + SCALE_BAR.setStrokeCap(Paint.Cap.SQUARE); + SCALE_BAR.setColor(Color.BLACK); + SCALE_BAR_STROKE.setStrokeWidth(5); + SCALE_BAR_STROKE.setStrokeCap(Paint.Cap.SQUARE); + SCALE_BAR_STROKE.setColor(Color.WHITE); + + SCALE_TEXT.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + SCALE_TEXT.setTextSize(17); + SCALE_TEXT.setColor(Color.BLACK); + SCALE_TEXT_STROKE.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + SCALE_TEXT_STROKE.setStyle(Paint.Style.STROKE); + SCALE_TEXT_STROKE.setColor(Color.WHITE); + SCALE_TEXT_STROKE.setStrokeWidth(2); + SCALE_TEXT_STROKE.setTextSize(17); + } + + private boolean mImperialUnits; + private MapPosition mMapPosition; + private final Bitmap mMapScaleBitmap; + private final Canvas mMapScaleCanvas; + private final MapView mMapView; + private boolean mRedrawNeeded; + private boolean mShowMapScaleBar; + private final Map mTextFields; + + MapScaleBar(MapView mapView) { + mMapView = mapView; + mMapScaleBitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_4444); + mMapScaleCanvas = new Canvas(mMapScaleBitmap); + mTextFields = new HashMap(); + setDefaultTexts(); + configurePaints(); + } + + /** + * @return true if imperial units are used, false otherwise. + */ + public boolean isImperialUnits() { + return mImperialUnits; + } + + /** + * @return true if this map scale bar is visible, false otherwise. + */ + public boolean isShowMapScaleBar() { + return mShowMapScaleBar; + } + + /** + * @param imperialUnits + * true if imperial units should be used rather than metric units. + */ + public void setImperialUnits(boolean imperialUnits) { + mImperialUnits = imperialUnits; + mRedrawNeeded = true; + } + + /** + * @param showMapScaleBar + * true if the map scale bar should be drawn, false otherwise. + */ + public void setShowMapScaleBar(boolean showMapScaleBar) { + mShowMapScaleBar = showMapScaleBar; + } + + /** + * Overrides the specified text field with the given string. + * + * @param textField + * the text field to override. + * @param value + * the new value of the text field. + */ + public void setText(TextField textField, String value) { + mTextFields.put(textField, value); + mRedrawNeeded = true; + } + + private void drawScaleBar(float scaleBarLength, Paint paint) { + mMapScaleCanvas.drawLine(7, 25, scaleBarLength + 3, 25, paint); + mMapScaleCanvas.drawLine(5, 10, 5, 40, paint); + mMapScaleCanvas.drawLine(scaleBarLength + 5, 10, scaleBarLength + 5, 40, paint); + } + + private void drawScaleText(int scaleValue, String unitSymbol, Paint paint) { + mMapScaleCanvas.drawText(scaleValue + unitSymbol, 12, 18, paint); + } + + private boolean isRedrawNecessary() { + if (mRedrawNeeded || mMapPosition == null) { + return true; + } + + MapPosition currentMapPosition = mMapView.getMapPosition().getMapPosition(); + + if (currentMapPosition.zoomLevel != mMapPosition.zoomLevel) { + return true; + } + + double latitudeDiff = Math.abs(currentMapPosition.geoPoint.getLatitude() + - mMapPosition.geoPoint.getLatitude()); + if (latitudeDiff > LATITUDE_REDRAW_THRESHOLD) { + return true; + } + + return false; + } + + /** + * Redraws the map scale bitmap with the given parameters. + * + * @param scaleBarLength + * the length of the map scale bar in pixels. + * @param mapScaleValue + * the map scale value in meters. + */ + private void redrawMapScaleBitmap(float scaleBarLength, int mapScaleValue) { + mMapScaleBitmap.eraseColor(Color.TRANSPARENT); + + // draw the scale bar + drawScaleBar(scaleBarLength, SCALE_BAR_STROKE); + drawScaleBar(scaleBarLength, SCALE_BAR); + + int scaleValue; + String unitSymbol; + if (mImperialUnits) { + if (mapScaleValue < ONE_MILE) { + scaleValue = mapScaleValue; + unitSymbol = mTextFields.get(TextField.FOOT); + } else { + scaleValue = mapScaleValue / ONE_MILE; + unitSymbol = mTextFields.get(TextField.MILE); + } + } else { + if (mapScaleValue < ONE_KILOMETER) { + scaleValue = mapScaleValue; + unitSymbol = mTextFields.get(TextField.METER); + } else { + scaleValue = mapScaleValue / ONE_KILOMETER; + unitSymbol = mTextFields.get(TextField.KILOMETER); + } + } + + // draw the scale text + drawScaleText(scaleValue, unitSymbol, SCALE_TEXT_STROKE); + drawScaleText(scaleValue, unitSymbol, SCALE_TEXT); + } + + private void setDefaultTexts() { + mTextFields.put(TextField.FOOT, " ft"); + mTextFields.put(TextField.MILE, " mi"); + + mTextFields.put(TextField.METER, " m"); + mTextFields.put(TextField.KILOMETER, " km"); + } + + void destroy() { + mMapScaleBitmap.recycle(); + } + + void draw(Canvas canvas) { + int top = mMapView.getHeight() - BITMAP_HEIGHT - MARGIN_BOTTOM; + canvas.drawBitmap(mMapScaleBitmap, MARGIN_LEFT, top, null); + } + + void redrawScaleBar() { + if (!isRedrawNecessary()) { + return; + } + + mMapPosition = mMapView.getMapPosition().getMapPosition(); + double groundResolution = MercatorProjection.calculateGroundResolution(mMapPosition.geoPoint.getLatitude(), + mMapPosition.zoomLevel); + + int[] scaleBarValues; + if (mImperialUnits) { + groundResolution = groundResolution / METER_FOOT_RATIO; + scaleBarValues = SCALE_BAR_VALUES_IMPERIAL; + } else { + scaleBarValues = SCALE_BAR_VALUES_METRIC; + } + + float scaleBarLength = 0; + int mapScaleValue = 0; + + for (int i = 0; i < scaleBarValues.length; ++i) { + mapScaleValue = scaleBarValues[i]; + scaleBarLength = mapScaleValue / (float) groundResolution; + if (scaleBarLength < (BITMAP_WIDTH - 10)) { + break; + } + } + + redrawMapScaleBitmap(scaleBarLength, mapScaleValue); + mRedrawNeeded = false; + } +} diff --git a/src/org/mapsforge/android/MapView.java b/src/org/mapsforge/android/MapView.java new file mode 100644 index 00000000..53f35369 --- /dev/null +++ b/src/org/mapsforge/android/MapView.java @@ -0,0 +1,709 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import java.io.File; +import java.io.FileNotFoundException; + +import org.mapsforge.android.inputhandling.MapMover; +import org.mapsforge.android.inputhandling.TouchHandler; +import org.mapsforge.android.inputhandling.ZoomAnimator; +import org.mapsforge.android.mapgenerator.JobParameters; +import org.mapsforge.android.mapgenerator.JobQueue; +import org.mapsforge.android.mapgenerator.JobTheme; +import org.mapsforge.android.mapgenerator.MapDatabaseFactory; +import org.mapsforge.android.mapgenerator.MapDatabaseInternal; +import org.mapsforge.android.mapgenerator.MapGenerator; +import org.mapsforge.android.mapgenerator.MapGeneratorFactory; +import org.mapsforge.android.mapgenerator.MapWorker; +import org.mapsforge.android.rendertheme.ExternalRenderTheme; +import org.mapsforge.android.rendertheme.InternalRenderTheme; +import org.mapsforge.android.utils.GlConfigChooser; +import org.mapsforge.core.GeoPoint; +import org.mapsforge.core.MapPosition; +import org.mapsforge.mapdatabase.FileOpenResult; +import org.mapsforge.mapdatabase.IMapDatabase; +import org.mapsforge.mapdatabase.mapfile.MapDatabase; + +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** + * A MapView shows a map on the display of the device. It handles all user input and touch gestures to move and zoom the + * map. This MapView also includes a scale bar and zoom controls. The {@link #getController()} method returns a + * {@link MapController} to programmatically modify the position and zoom level of the map. + *

+ * This implementation supports offline map rendering as well as downloading map images (tiles) over an Internet + * connection. The operation mode of a MapView can be set in the constructor and changed at runtime with the + * {@link #setMapGeneratorInternal(MapGenerator)} method. Some MapView parameters depend on the selected operation mode. + *

+ * In offline rendering mode a special database file is required which contains the map data. Map files can be stored in + * any folder. The current map file is set by calling {@link #setMapFile(String)}. To retrieve the current + * {@link MapDatabase}, use the {@link #getMapDatabase()} method. + *

+ */ +public class MapView extends GLSurfaceView { + + final static String TAG = "MapView"; + + /** + * Default render theme of the MapView. + */ + public static final InternalRenderTheme DEFAULT_RENDER_THEME = InternalRenderTheme.OSMARENDER; + + private static final float DEFAULT_TEXT_SCALE = 1; + + private final MapController mMapController; + private final MapMover mMapMover; + private final MapScaleBar mMapScaleBar; + private final MapViewPosition mMapViewPosition; + + private final MapZoomControls mMapZoomControls; + private final Projection mProjection; + private final TouchHandler mTouchEventHandler; + private final ZoomAnimator mZoomAnimator; + + private IMapDatabase mMapDatabase; + private MapGenerator mMapGenerator; + private MapRenderer mMapRenderer; + private JobQueue mJobQueue; + private MapWorker mMapWorker; + private JobParameters mJobParameters; + private DebugSettings mDebugSettings; + private String mMapFile; + + /** + * @param context + * the enclosing MapActivity instance. + * @throws IllegalArgumentException + * if the context object is not an instance of {@link MapActivity} . + */ + public MapView(Context context) { + this(context, null, new org.mapsforge.android.glrenderer.DatabaseRenderer()); + } + + /** + * @param context + * the enclosing MapActivity instance. + * @param attributeSet + * a set of attributes. + * @throws IllegalArgumentException + * if the context object is not an instance of {@link MapActivity} . + */ + public MapView(Context context, AttributeSet attributeSet) { + this(context, attributeSet, MapGeneratorFactory.createMapGenerator(attributeSet)); + } + + /** + * @param context + * the enclosing MapActivity instance. + * @param mapGenerator + * the MapGenerator for this MapView. + * @throws IllegalArgumentException + * if the context object is not an instance of {@link MapActivity} . + */ + public MapView(Context context, MapGenerator mapGenerator) { + this(context, null, mapGenerator); + } + + private MapView(Context context, AttributeSet attributeSet, MapGenerator mapGenerator) { + + super(context, attributeSet); + + if (!(context instanceof MapActivity)) { + throw new IllegalArgumentException("context is not an instance of MapActivity"); + } + setWillNotDraw(true); + setWillNotCacheDrawing(true); + + MapActivity mapActivity = (MapActivity) context; + + mDebugSettings = new DebugSettings(false, false, false); + + mJobParameters = new JobParameters(DEFAULT_RENDER_THEME, DEFAULT_TEXT_SCALE); + mMapController = new MapController(this); + + // mMapDatabase = MapDatabaseFactory.createMapDatabase(MapDatabaseInternal.POSTGIS_READER); + mMapDatabase = MapDatabaseFactory.createMapDatabase(MapDatabaseInternal.MAP_READER); + + mMapViewPosition = new MapViewPosition(this); + mMapScaleBar = new MapScaleBar(this); + mMapZoomControls = new MapZoomControls(mapActivity, this); + mProjection = new MapViewProjection(this); + mTouchEventHandler = new TouchHandler(mapActivity, this); + + mJobQueue = new JobQueue(this); + mMapWorker = new MapWorker(this); + mMapWorker.start(); + mMapMover = new MapMover(this); + mMapMover.start(); + mZoomAnimator = new ZoomAnimator(this); + mZoomAnimator.start(); + + setMapGeneratorInternal(mapGenerator); + + GeoPoint startPoint = mMapGenerator.getStartPoint(); + if (startPoint != null) { + mMapViewPosition.setMapCenter(startPoint); + } + + Byte startZoomLevel = mMapGenerator.getStartZoomLevel(); + if (startZoomLevel != null) { + mMapViewPosition.setZoomLevel(startZoomLevel.byteValue()); + } + + mapActivity.registerMapView(this); + + setEGLConfigChooser(new GlConfigChooser()); + setEGLContextClientVersion(2); + + mMapRenderer = mMapGenerator.getMapRenderer(this); + setRenderer(mMapRenderer); + + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + mMapWorker.setMapRenderer(mMapRenderer); + } + + /** + * @return the MapController for this MapView. + */ + public MapController getController() { + return mMapController; + } + + /** + * @return the debug settings which are used in this MapView. + */ + public DebugSettings getDebugSettings() { + return mDebugSettings; + } + + /** + * @return the job queue which is used in this MapView. + */ + public JobQueue getJobQueue() { + return mJobQueue; + } + + /** + * @return the map database which is used for reading map files. + * @throws UnsupportedOperationException + * if the current MapGenerator works with an Internet connection. + */ + public IMapDatabase getMapDatabase() { + if (mMapGenerator.requiresInternetConnection()) { + throw new UnsupportedOperationException(); + } + return mMapDatabase; + } + + /** + * @return the currently used map file. + * @throws UnsupportedOperationException + * if the current MapGenerator mode works with an Internet connection. + */ + public String getMapFile() { + if (mMapGenerator.requiresInternetConnection()) { + throw new UnsupportedOperationException(); + } + return mMapFile; + } + + /** + * @return the currently used MapGenerator (may be null). + */ + public MapGenerator getMapGenerator() { + return mMapGenerator; + } + + /** + * @return the MapMover which is used by this MapView. + */ + public MapMover getMapMover() { + return mMapMover; + } + + /** + * @return the current position and zoom level of this MapView. + */ + public MapViewPosition getMapPosition() { + return mMapViewPosition; + } + + /** + * @return the scale bar which is used in this MapView. + */ + public MapScaleBar getMapScaleBar() { + return mMapScaleBar; + } + + /** + * @return the zoom controls instance which is used in this MapView. + */ + public MapZoomControls getMapZoomControls() { + return mMapZoomControls; + } + + /** + * @return the currently used projection of the map. Do not keep this object for a longer time. + */ + public Projection getProjection() { + return mProjection; + } + + /** + * @return true if the ZoomAnimator is currently running, false otherwise. + */ + public boolean isZoomAnimatorRunning() { + return mZoomAnimator.isExecuting(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { + return mMapMover.onKeyDown(keyCode, keyEvent); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { + return mMapMover.onKeyUp(keyCode, keyEvent); + } + + @Override + public boolean onTouchEvent(MotionEvent motionEvent) { + return mTouchEventHandler.handleMotionEvent(motionEvent); + } + + @Override + public boolean onTrackballEvent(MotionEvent motionEvent) { + return mMapMover.onTrackballEvent(motionEvent); + } + + /** + * Calculates all necessary tiles and adds jobs accordingly. + */ + public synchronized void redrawTiles() { + if (getWidth() <= 0 || getHeight() <= 0) + return; + + mMapRenderer.redrawTiles(false); + } + + void clearAndRedrawMapView() { + if (getWidth() <= 0 || getHeight() <= 0) + return; + + mMapRenderer.redrawTiles(true); + } + + /** + * Sets the visibility of the zoom controls. + * + * @param showZoomControls + * true if the zoom controls should be visible, false otherwise. + */ + public void setBuiltInZoomControls(boolean showZoomControls) { + mMapZoomControls.setShowMapZoomControls(showZoomControls); + + } + + /** + * Sets the center of the MapView and triggers a redraw. + * + * @param geoPoint + * the new center point of the map. + */ + public void setCenter(GeoPoint geoPoint) { + MapPosition mapPosition = new MapPosition(geoPoint, mMapViewPosition.getZoomLevel(), 1); + setCenterAndZoom(mapPosition); + } + + /** + * @param debugSettings + * the new DebugSettings for this MapView. + */ + public void setDebugSettings(DebugSettings debugSettings) { + mDebugSettings = debugSettings; + + clearAndRedrawMapView(); + } + + /** + * Sets the map file for this MapView. + * + * @param mapFile + * the path to the map file. + * @return true if the map file was set correctly, false otherwise. + * @throws UnsupportedOperationException + * if the current MapGenerator mode works with an Internet connection. + */ + public boolean setMapFile(String mapFile) { + if (mMapGenerator.requiresInternetConnection()) { + throw new UnsupportedOperationException(); + } + Log.d(TAG, "set mapfile " + mapFile); + if (mapFile == null) { + // no map file specified + return false; + } else if (mapFile.equals(mMapFile)) { + // same map file as before + return false; + } + + mZoomAnimator.pause(); + mMapWorker.pause(); + mMapMover.pause(); + mZoomAnimator.awaitPausing(); + mMapMover.awaitPausing(); + mMapWorker.awaitPausing(); + mMapMover.stopMove(); + + mZoomAnimator.proceed(); + mMapWorker.proceed(); + mMapMover.proceed(); + + mMapDatabase.closeFile(); + FileOpenResult fileOpenResult = mMapDatabase.openFile(new File(mapFile)); + if (fileOpenResult.isSuccess()) { + mMapFile = mapFile; + + GeoPoint startPoint = mMapGenerator.getStartPoint(); + if (startPoint != null) { + Log.d(TAG, "mapfile got startpoint"); + mMapViewPosition.setMapCenter(startPoint); + } + + Byte startZoomLevel = mMapGenerator.getStartZoomLevel(); + if (startZoomLevel != null) { + Log.d(TAG, "mapfile got start zoomlevel"); + mMapViewPosition.setZoomLevel(startZoomLevel.byteValue()); + } + + clearAndRedrawMapView(); + Log.d(TAG, "mapfile set"); + return true; + } + mMapFile = null; + clearAndRedrawMapView(); + Log.d(TAG, "loading mapfile failed"); + return false; + } + + /** + * Sets the MapGenerator for this MapView. + * + * @param mapGenerator + * the new MapGenerator. + */ + public void setMapGenerator(MapGenerator mapGenerator) { + + if (mMapGenerator != mapGenerator) { + setMapGeneratorInternal(mapGenerator); + + clearAndRedrawMapView(); + } + } + + private void setMapGeneratorInternal(MapGenerator mapGenerator) { + if (mapGenerator == null) { + throw new IllegalArgumentException("mapGenerator must not be null"); + } + + mapGenerator.setMapDatabase(mMapDatabase); + + mMapGenerator = mapGenerator; + mMapWorker.setMapGenerator(mMapGenerator); + + } + + /** + * Sets the MapDatabase for this MapView. + * + * @param mapDatabase + * the new MapDatabase. + */ + public void setMapDatabase(IMapDatabase mapDatabase) { + Log.d(TAG, "setMapDatabase " + mapDatabase.getClass()); + if (mMapDatabase != mapDatabase) { + + if (mMapDatabase != null) + mMapDatabase.closeFile(); + + setMapDatabaseInternal(mapDatabase); + + // clearAndRedrawMapView(); + } + } + + private void setMapDatabaseInternal(IMapDatabase mapDatabase) { + if (mapDatabase == null) { + throw new IllegalArgumentException("MapDatabase must not be null"); + } + // mMapWorker.pause(); + // mMapWorker.awaitPausing(); + + mMapDatabase = mapDatabase; + mMapGenerator.setMapDatabase(mMapDatabase); + + Log.d(TAG, "setMapDatabaseInternal " + mapDatabase.getClass()); + // mMapWorker.proceed(); + + String mapFile = mMapFile; + mMapFile = null; + setMapFile(mapFile); + + // mMapWorker.setMapDatabase(mMapDatabase); + + } + + /** + * Sets the internal theme which is used for rendering the map. + * + * @param internalRenderTheme + * the internal rendering theme. + * @throws IllegalArgumentException + * if the supplied internalRenderTheme is null. + * @throws UnsupportedOperationException + * if the current MapGenerator does not support render themes. + */ + public void setRenderTheme(InternalRenderTheme internalRenderTheme) { + if (internalRenderTheme == null) { + throw new IllegalArgumentException("render theme must not be null"); + } else if (mMapGenerator.requiresInternetConnection()) { + throw new UnsupportedOperationException(); + } + Log.d(TAG, "set rendertheme " + internalRenderTheme); + mJobParameters = new JobParameters(internalRenderTheme, mJobParameters.textScale); + + clearAndRedrawMapView(); + } + + /** + * Sets the theme file which is used for rendering the map. + * + * @param renderThemePath + * the path to the XML file which defines the rendering theme. + * @throws IllegalArgumentException + * if the supplied internalRenderTheme is null. + * @throws UnsupportedOperationException + * if the current MapGenerator does not support render themes. + * @throws FileNotFoundException + * if the supplied file does not exist, is a directory or cannot be read. + */ + public void setRenderTheme(String renderThemePath) throws FileNotFoundException { + if (renderThemePath == null) { + throw new IllegalArgumentException("render theme path must not be null"); + } else if (mMapGenerator.requiresInternetConnection()) { + throw new UnsupportedOperationException(); + } + + JobTheme jobTheme = new ExternalRenderTheme(renderThemePath); + mJobParameters = new JobParameters(jobTheme, mJobParameters.textScale); + + clearAndRedrawMapView(); + } + + /** + * Sets the text scale for the map rendering. Has no effect in downloading mode. + * + * @param textScale + * the new text scale for the map rendering. + */ + public void setTextScale(float textScale) { + mJobParameters = new JobParameters(mJobParameters.jobTheme, textScale); + clearAndRedrawMapView(); + } + + /** + * Zooms in or out by the given amount of zoom levels. + * + * @param zoomLevelDiff + * the difference to the current zoom level. + * @return true if the zoom level was changed, false otherwise. + */ + public boolean zoom(byte zoomLevelDiff) { + + int z = mMapViewPosition.getZoomLevel() + zoomLevelDiff; + if (zoomLevelDiff > 0) { + // check if zoom in is possible + if (z > getMaximumPossibleZoomLevel()) { + return false; + } + + } else if (zoomLevelDiff < 0) { + // check if zoom out is possible + if (z < mMapZoomControls.getZoomLevelMin()) { + return false; + } + } + + mMapViewPosition.setZoomLevel((byte) z); + + redrawTiles(); + + return true; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + // mMapZoomControls.onLayout(changed, left, top, right, bottom); + } + + @Override + protected final void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // find out how big the zoom controls should be + mMapZoomControls.measure( + MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.AT_MOST)); + + // make sure that MapView is big enough to display the zoom controls + setMeasuredDimension(Math.max(MeasureSpec.getSize(widthMeasureSpec), mMapZoomControls.getMeasuredWidth()), + Math.max(MeasureSpec.getSize(heightMeasureSpec), mMapZoomControls.getMeasuredHeight())); + } + + @Override + protected synchronized void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + mMapWorker.pause(); + mMapWorker.awaitPausing(); + super.onSizeChanged(width, height, oldWidth, oldHeight); + mMapWorker.proceed(); + + // redrawTiles(); + } + + void destroy() { + mMapMover.interrupt(); + mMapWorker.interrupt(); + mZoomAnimator.interrupt(); + + try { + mMapWorker.join(); + } catch (InterruptedException e) { + // restore the interrupted status + Thread.currentThread().interrupt(); + } + + mMapScaleBar.destroy(); + mMapDatabase.closeFile(); + + } + + /** + * @return the maximum possible zoom level. + */ + byte getMaximumPossibleZoomLevel() { + return (byte) Math.min(mMapZoomControls.getZoomLevelMax(), mMapGenerator.getZoomLevelMax()); + } + + /** + * @return true if the current center position of this MapView is valid, false otherwise. + */ + boolean hasValidCenter() { + if (!mMapViewPosition.isValid()) { + return false; + } else if (!mMapGenerator.requiresInternetConnection() && + (!mMapDatabase.hasOpenFile() || + !mMapDatabase.getMapFileInfo().boundingBox + .contains(getMapPosition().getMapCenter()))) { + return false; + } + + return true; + } + + byte limitZoomLevel(byte zoom) { + return (byte) Math.max(Math.min(zoom, getMaximumPossibleZoomLevel()), mMapZoomControls.getZoomLevelMin()); + } + + @Override + public void onPause() { + super.onPause(); + mMapWorker.pause(); + mMapMover.pause(); + mZoomAnimator.pause(); + } + + @Override + public void onResume() { + super.onResume(); + mMapWorker.proceed(); + mMapMover.proceed(); + mZoomAnimator.proceed(); + } + + /** + * Sets the center and zoom level of this MapView and triggers a redraw. + * + * @param mapPosition + * the new map position of this MapView. + */ + void setCenterAndZoom(MapPosition mapPosition) { + + // if (hasValidCenter()) { + // // calculate the distance between previous and current position + // MapPosition mapPositionOld = mapViewPosition.getMapPosition(); + + // GeoPoint geoPointOld = mapPositionOld.geoPoint; + // GeoPoint geoPointNew = mapPosition.geoPoint; + // double oldPixelX = + // MercatorProjection.longitudeToPixelX(geoPointOld.getLongitude(), + // mapPositionOld.zoomLevel); + // double newPixelX = + // MercatorProjection.longitudeToPixelX(geoPointNew.getLongitude(), + // mapPosition.zoomLevel); + // + // double oldPixelY = + // MercatorProjection.latitudeToPixelY(geoPointOld.getLatitude(), + // mapPositionOld.zoomLevel); + // double newPixelY = + // MercatorProjection.latitudeToPixelY(geoPointNew.getLatitude(), + // mapPosition.zoomLevel); + + // float matrixTranslateX = (float) (oldPixelX - newPixelX); + // float matrixTranslateY = (float) (oldPixelY - newPixelY); + // frameBuffer.matrixPostTranslate(matrixTranslateX, + // matrixTranslateY); + // } + // + mMapViewPosition.setMapCenterAndZoomLevel(mapPosition); + // mapZoomControls.onZoomLevelChange(mapViewPosition.getZoomLevel()); + redrawTiles(); + } + + /** + * @return MapPosition + */ + public MapViewPosition getMapViewPosition() { + return mMapViewPosition; + } + + /** + * @return current JobParameters + */ + public JobParameters getJobParameters() { + return mJobParameters; + } + + /** + * @return MapWorker + */ + public MapWorker getMapWorker() { + return mMapWorker; + } +} diff --git a/src/org/mapsforge/android/MapViewPosition.java b/src/org/mapsforge/android/MapViewPosition.java new file mode 100644 index 00000000..28ca0f15 --- /dev/null +++ b/src/org/mapsforge/android/MapViewPosition.java @@ -0,0 +1,230 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import org.mapsforge.core.GeoPoint; +import org.mapsforge.core.MapPosition; +import org.mapsforge.core.MercatorProjection; + +import android.util.FloatMath; + +/** + * A MapPosition stores the latitude and longitude coordinate of a MapView together with its zoom level. + */ +public class MapViewPosition { + private static float MAX_SCALE = 2.0f; + private static float MIN_SCALE = 1.0f; + + private double mLatitude; + private double mLongitude; + private final MapView mMapView; + private byte mZoomLevel; + private float mScale; + + // private float mRotation; + + MapViewPosition(MapView mapView) { + mMapView = mapView; + + mLatitude = Double.NaN; + mLongitude = Double.NaN; + mZoomLevel = -1; + mScale = 1; + // mRotation = 0.0f; + } + + /** + * @return the current center point of the MapView. + */ + public synchronized GeoPoint getMapCenter() { + return new GeoPoint(mLatitude, mLongitude); + } + + /** + * @return an immutable MapPosition or null, if this map position is not valid. + * @see #isValid() + */ + public synchronized MapPosition getMapPosition() { + if (!isValid()) { + return null; + } + GeoPoint geoPoint = new GeoPoint(mLatitude, mLongitude); + return new MapPosition(geoPoint, mZoomLevel, mScale); + } + + /** + * @return the current zoom level of the MapView. + */ + public synchronized byte getZoomLevel() { + return mZoomLevel; + } + + /** + * @return the current scale of the MapView. + */ + public synchronized float getScale() { + return mScale; + } + + /** + * @return true if this MapViewPosition is valid, false otherwise. + */ + public synchronized boolean isValid() { + if (Double.isNaN(mLatitude)) { + return false; + } else if (mLatitude < MercatorProjection.LATITUDE_MIN) { + return false; + } else if (mLatitude > MercatorProjection.LATITUDE_MAX) { + return false; + } + + if (Double.isNaN(mLongitude)) { + return false; + } else if (mLongitude < MercatorProjection.LONGITUDE_MIN) { + return false; + } else if (mLongitude > MercatorProjection.LONGITUDE_MAX) { + return false; + } + + return true; + } + + /** + * Moves this MapViewPosition by the given amount of pixels. + * + * @param moveHorizontal + * the amount of pixels to move the map horizontally. + * @param moveVertical + * the amount of pixels to move the map vertically. + */ + public synchronized void moveMap(float moveHorizontal, float moveVertical) { + double pixelX = MercatorProjection.longitudeToPixelX(mLongitude, mZoomLevel); + double pixelY = MercatorProjection.latitudeToPixelY(mLatitude, mZoomLevel); + + mLatitude = MercatorProjection.pixelYToLatitude(pixelY - moveVertical / mScale, + mZoomLevel); + mLatitude = MercatorProjection.limitLatitude(mLatitude); + + mLongitude = MercatorProjection.pixelXToLongitude(pixelX - moveHorizontal / mScale, + mZoomLevel); + mLongitude = MercatorProjection.limitLongitude(mLongitude); + } + + // public synchronized void rotateMap(float angle) { + // mRotation = angle; + // } + + synchronized void setMapCenter(GeoPoint geoPoint) { + mLatitude = MercatorProjection.limitLatitude(geoPoint.getLatitude()); + mLongitude = MercatorProjection.limitLongitude(geoPoint.getLongitude()); + } + + synchronized void setMapCenterAndZoomLevel(MapPosition mapPosition) { + GeoPoint geoPoint = mapPosition.geoPoint; + mLatitude = MercatorProjection.limitLatitude(geoPoint.getLatitude()); + mLongitude = MercatorProjection.limitLongitude(geoPoint.getLongitude()); + mZoomLevel = mMapView.limitZoomLevel(mapPosition.zoomLevel); + } + + synchronized void setZoomLevel(byte zoomLevel) { + mZoomLevel = mMapView.limitZoomLevel(zoomLevel); + } + + synchronized void setScale(float scale) { + mScale = scale; + } + + /** + * @param scale + * ... + * @param pivotX + * ... + * @param pivotY + * ... + */ + public synchronized void scaleMap(float scale, float pivotX, float pivotY) { + moveMap(pivotX * (1.0f - scale), + pivotY * (1.0f - scale)); + + float s = mScale * scale; + + if (s >= MAX_SCALE) { + + byte z = (byte) FloatMath.sqrt(s); + mZoomLevel += z; + s *= 1.0f / (1 << z); + } else if (s < MIN_SCALE) { + byte z = (byte) FloatMath.sqrt(1 / s); + mZoomLevel -= z; + s *= 1 << z; + } + + mScale = s; + } + + /** + * Zooms in or out by the given amount of zoom levels. + * + * @param zoomLevelDiff + * the difference to the current zoom level. + * @param s + * scale between min/max zoom + * @return true if the zoom level was changed, false otherwise. + */ + // public boolean zoom(byte zoomLevelDiff, float s) { + // float scale = s; + // + // if (zoomLevelDiff > 0) { + // // check if zoom in is possible + // if (mMapViewPosition.getZoomLevel() + zoomLevelDiff > getMaximumPossibleZoomLevel()) { + // return false; + // } + // + // scale *= 1.0f / (1 << zoomLevelDiff); + // } else if (zoomLevelDiff < 0) { + // // check if zoom out is possible + // if (mMapViewPosition.getZoomLevel() + zoomLevelDiff < mMapZoomControls.getZoomLevelMin()) { + // return false; + // } + // + // scale *= 1 << -zoomLevelDiff; + // } + // + // if (scale == 0) + // scale = 1; + // // else + // // scale = Math.round(256.0f * scale) / 256.0f; + // + // mMapViewPosition.setZoomLevel((byte) (mMapViewPosition.getZoomLevel() + zoomLevelDiff)); + // + // // mapZoomControls.onZoomLevelChange(mapViewPosition.getZoomLevel()); + // + // // zoomAnimator.setParameters(zoomStart, matrixScaleFactor, + // // getWidth() >> 1, getHeight() >> 1); + // // zoomAnimator.startAnimation(); + // + // // if (scale > MAX_ZOOM) { + // // scale = MAX_ZOOM; + // // } + // + // if (zoomLevelDiff != 0 || mZoomFactor != scale) { + // mZoomFactor = scale; + // redrawTiles(); + // } + // + // return true; + // } + +} diff --git a/src/org/mapsforge/android/MapViewProjection.java b/src/org/mapsforge/android/MapViewProjection.java new file mode 100644 index 00000000..1685df27 --- /dev/null +++ b/src/org/mapsforge/android/MapViewProjection.java @@ -0,0 +1,120 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import org.mapsforge.core.GeoPoint; +import org.mapsforge.core.MapPosition; +import org.mapsforge.core.MercatorProjection; + +import android.graphics.Point; + +class MapViewProjection implements Projection { + private static final String INVALID_MAP_VIEW_DIMENSIONS = "invalid MapView dimensions"; + + private final MapView mMapView; + + MapViewProjection(MapView mapView) { + mMapView = mapView; + } + + @Override + public GeoPoint fromPixels(int x, int y) { + if (mMapView.getWidth() <= 0 || mMapView.getHeight() <= 0) { + return null; + } + + MapPosition mapPosition = mMapView.getMapPosition().getMapPosition(); + + // calculate the pixel coordinates of the top left corner + GeoPoint geoPoint = mapPosition.geoPoint; + double pixelX = MercatorProjection.longitudeToPixelX(geoPoint.getLongitude(), mapPosition.zoomLevel); + double pixelY = MercatorProjection.latitudeToPixelY(geoPoint.getLatitude(), mapPosition.zoomLevel); + pixelX -= mMapView.getWidth() >> 1; + pixelY -= mMapView.getHeight() >> 1; + + // convert the pixel coordinates to a GeoPoint and return it + return new GeoPoint(MercatorProjection.pixelYToLatitude(pixelY + y, mapPosition.zoomLevel), + MercatorProjection.pixelXToLongitude(pixelX + x, mapPosition.zoomLevel)); + } + + @Override + public int getLatitudeSpan() { + if (mMapView.getWidth() > 0 && mMapView.getWidth() > 0) { + GeoPoint top = fromPixels(0, 0); + GeoPoint bottom = fromPixels(0, mMapView.getHeight()); + return Math.abs(top.latitudeE6 - bottom.latitudeE6); + } + throw new IllegalStateException(INVALID_MAP_VIEW_DIMENSIONS); + } + + @Override + public int getLongitudeSpan() { + if (mMapView.getWidth() > 0 && mMapView.getWidth() > 0) { + GeoPoint left = fromPixels(0, 0); + GeoPoint right = fromPixels(mMapView.getWidth(), 0); + return Math.abs(left.longitudeE6 - right.longitudeE6); + } + throw new IllegalStateException(INVALID_MAP_VIEW_DIMENSIONS); + } + + @Override + public float metersToPixels(float meters, byte zoom) { + double latitude = mMapView.getMapPosition().getMapCenter().getLatitude(); + double groundResolution = MercatorProjection.calculateGroundResolution(latitude, zoom); + return (float) (meters * (1 / groundResolution)); + } + + @Override + public Point toPixels(GeoPoint in, Point out) { + if (mMapView.getWidth() <= 0 || mMapView.getHeight() <= 0) { + return null; + } + + MapPosition mapPosition = mMapView.getMapPosition().getMapPosition(); + + // calculate the pixel coordinates of the top left corner + GeoPoint geoPoint = mapPosition.geoPoint; + double pixelX = MercatorProjection.longitudeToPixelX(geoPoint.getLongitude(), mapPosition.zoomLevel); + double pixelY = MercatorProjection.latitudeToPixelY(geoPoint.getLatitude(), mapPosition.zoomLevel); + pixelX -= mMapView.getWidth() >> 1; + pixelY -= mMapView.getHeight() >> 1; + + if (out == null) { + // create a new point and return it + return new Point( + (int) (MercatorProjection.longitudeToPixelX(in.getLongitude(), mapPosition.zoomLevel) - pixelX), + (int) (MercatorProjection.latitudeToPixelY(in.getLatitude(), mapPosition.zoomLevel) - pixelY)); + } + + // reuse the existing point + out.x = (int) (MercatorProjection.longitudeToPixelX(in.getLongitude(), mapPosition.zoomLevel) - pixelX); + out.y = (int) (MercatorProjection.latitudeToPixelY(in.getLatitude(), mapPosition.zoomLevel) - pixelY); + return out; + } + + @Override + public Point toPoint(GeoPoint in, Point out, byte zoom) { + if (out == null) { + // create a new point and return it + return new Point((int) MercatorProjection.longitudeToPixelX(in.getLongitude(), zoom), + (int) MercatorProjection.latitudeToPixelY(in.getLatitude(), zoom)); + } + + // reuse the existing point + out.x = (int) MercatorProjection.longitudeToPixelX(in.getLongitude(), zoom); + out.y = (int) MercatorProjection.latitudeToPixelY(in.getLatitude(), zoom); + return out; + } +} diff --git a/src/org/mapsforge/android/MapZoomControls.java b/src/org/mapsforge/android/MapZoomControls.java new file mode 100644 index 00000000..16e46d21 --- /dev/null +++ b/src/org/mapsforge/android/MapZoomControls.java @@ -0,0 +1,309 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import org.mapsforge.android.mapgenerator.MapGenerator; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.ZoomControls; + +/** + * A MapZoomControls instance displays buttons for zooming in and out in a map. + */ +public class MapZoomControls { + private static class ZoomControlsHideHandler extends Handler { + private final ZoomControls mZoomControls; + + ZoomControlsHideHandler(ZoomControls zoomControls) { + super(); + mZoomControls = zoomControls; + } + + @Override + public void handleMessage(Message message) { + mZoomControls.hide(); + } + } + + private static class ZoomInClickListener implements View.OnClickListener { + private final MapView mMapView; + + ZoomInClickListener(MapView mapView) { + mMapView = mapView; + } + + @Override + public void onClick(View view) { + mMapView.zoom((byte) 1); + } + } + + private static class ZoomOutClickListener implements View.OnClickListener { + private final MapView mMapView; + + ZoomOutClickListener(MapView mapView) { + mMapView = mapView; + } + + @Override + public void onClick(View view) { + mMapView.zoom((byte) -1); + } + } + + /** + * Default {@link Gravity} of the zoom controls. + */ + private static final int DEFAULT_ZOOM_CONTROLS_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT; + + /** + * Default maximum zoom level. + */ + private static final byte DEFAULT_ZOOM_LEVEL_MAX = 22; + + /** + * Default minimum zoom level. + */ + private static final byte DEFAULT_ZOOM_LEVEL_MIN = 0; + + /** + * Message code for the handler to hide the zoom controls. + */ + private static final int MSG_ZOOM_CONTROLS_HIDE = 0; + + /** + * Horizontal padding for the zoom controls. + */ + private static final int ZOOM_CONTROLS_HORIZONTAL_PADDING = 5; + + /** + * Delay in milliseconds after which the zoom controls disappear. + */ + private static final long ZOOM_CONTROLS_TIMEOUT = ViewConfiguration.getZoomControlsTimeout(); + + private boolean mGravityChanged; + private boolean mShowMapZoomControls; + private final ZoomControls mZoomControls; + private int mZoomControlsGravity; + private final Handler mZoomControlsHideHandler; + private byte mZoomLevelMax; + private byte mZoomLevelMin; + + MapZoomControls(Context context, final MapView mapView) { + mZoomControls = new ZoomControls(context); + mShowMapZoomControls = true; + mZoomLevelMax = DEFAULT_ZOOM_LEVEL_MAX; + mZoomLevelMin = DEFAULT_ZOOM_LEVEL_MIN; + mZoomControls.setVisibility(View.GONE); + mZoomControlsGravity = DEFAULT_ZOOM_CONTROLS_GRAVITY; + + mZoomControls.setOnZoomInClickListener(new ZoomInClickListener(mapView)); + mZoomControls.setOnZoomOutClickListener(new ZoomOutClickListener(mapView)); + mZoomControlsHideHandler = new ZoomControlsHideHandler(mZoomControls); + + // int wrapContent = android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + // LayoutParams layoutParams = new LayoutParams(wrapContent, wrapContent); + // mapView.addView(zoomControls, layoutParams); + } + + /** + * @return the current gravity for the placing of the zoom controls. + * @see Gravity + */ + public int getZoomControlsGravity() { + return mZoomControlsGravity; + } + + /** + * @return the maximum zoom level of the map. + */ + public byte getZoomLevelMax() { + return mZoomLevelMax; + } + + /** + * @return the minimum zoom level of the map. + */ + public byte getZoomLevelMin() { + return mZoomLevelMin; + } + + /** + * @return true if the zoom controls are visible, false otherwise. + */ + public boolean isShowMapZoomControls() { + return mShowMapZoomControls; + } + + /** + * @param showMapZoomControls + * true if the zoom controls should be visible, false otherwise. + */ + public void setShowMapZoomControls(boolean showMapZoomControls) { + mShowMapZoomControls = false; // showMapZoomControls; + } + + /** + * Sets the gravity for the placing of the zoom controls. Supported values are {@link Gravity#TOP}, + * {@link Gravity#CENTER_VERTICAL}, {@link Gravity#BOTTOM}, {@link Gravity#LEFT}, {@link Gravity#CENTER_HORIZONTAL} + * and {@link Gravity#RIGHT}. + * + * @param zoomControlsGravity + * a combination of {@link Gravity} constants describing the desired placement. + */ + public void setZoomControlsGravity(int zoomControlsGravity) { + if (mZoomControlsGravity != zoomControlsGravity) { + mZoomControlsGravity = zoomControlsGravity; + mGravityChanged = true; + } + } + + /** + * Sets the maximum zoom level of the map. + *

+ * The maximum possible zoom level of the MapView depends also on the current {@link MapGenerator}. For example, + * downloading map tiles may only be possible up to a certain zoom level. Setting a higher maximum zoom level has no + * effect in this case. + * + * @param zoomLevelMax + * the maximum zoom level. + * @throws IllegalArgumentException + * if the maximum zoom level is smaller than the current minimum zoom level. + */ + public void setZoomLevelMax(byte zoomLevelMax) { + if (zoomLevelMax < mZoomLevelMin) { + throw new IllegalArgumentException(); + } + mZoomLevelMax = zoomLevelMax; + } + + /** + * Sets the minimum zoom level of the map. + * + * @param zoomLevelMin + * the minimum zoom level. + * @throws IllegalArgumentException + * if the minimum zoom level is larger than the current maximum zoom level. + */ + public void setZoomLevelMin(byte zoomLevelMin) { + if (zoomLevelMin > mZoomLevelMax) { + throw new IllegalArgumentException(); + } + mZoomLevelMin = zoomLevelMin; + } + + private int calculatePositionLeft(int left, int right, int zoomControlsWidth) { + int gravity = mZoomControlsGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + switch (gravity) { + case Gravity.LEFT: + return ZOOM_CONTROLS_HORIZONTAL_PADDING; + + case Gravity.CENTER_HORIZONTAL: + return (right - left - zoomControlsWidth) / 2; + + case Gravity.RIGHT: + return right - left - zoomControlsWidth - ZOOM_CONTROLS_HORIZONTAL_PADDING; + } + + throw new IllegalArgumentException("unknown horizontal gravity: " + gravity); + } + + private int calculatePositionTop(int top, int bottom, int zoomControlsHeight) { + int gravity = mZoomControlsGravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (gravity) { + case Gravity.TOP: + return 0; + + case Gravity.CENTER_VERTICAL: + return (bottom - top - zoomControlsHeight) / 2; + + case Gravity.BOTTOM: + return bottom - top - zoomControlsHeight; + } + + throw new IllegalArgumentException("unknown vertical gravity: " + gravity); + } + + private void showZoomControls() { + mZoomControlsHideHandler.removeMessages(MSG_ZOOM_CONTROLS_HIDE); + if (mZoomControls.getVisibility() != View.VISIBLE) { + mZoomControls.show(); + } + } + + private void showZoomControlsWithTimeout() { + showZoomControls(); + mZoomControlsHideHandler.sendEmptyMessageDelayed(MSG_ZOOM_CONTROLS_HIDE, ZOOM_CONTROLS_TIMEOUT); + } + + int getMeasuredHeight() { + return mZoomControls.getMeasuredHeight(); + } + + int getMeasuredWidth() { + return mZoomControls.getMeasuredWidth(); + } + + void measure(int widthMeasureSpec, int heightMeasureSpec) { + mZoomControls.measure(widthMeasureSpec, heightMeasureSpec); + } + + void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!changed && !mGravityChanged) { + return; + } + + int zoomControlsWidth = mZoomControls.getMeasuredWidth(); + int zoomControlsHeight = mZoomControls.getMeasuredHeight(); + + int positionLeft = calculatePositionLeft(left, right, zoomControlsWidth); + int positionTop = calculatePositionTop(top, bottom, zoomControlsHeight); + int positionRight = positionLeft + zoomControlsWidth; + int positionBottom = positionTop + zoomControlsHeight; + + mZoomControls.layout(positionLeft, positionTop, positionRight, positionBottom); + mGravityChanged = false; + } + + void onMapViewTouchEvent(int action) { + if (mShowMapZoomControls) { + switch (action) { + case MotionEvent.ACTION_DOWN: + showZoomControls(); + break; + case MotionEvent.ACTION_CANCEL: + showZoomControlsWithTimeout(); + break; + case MotionEvent.ACTION_UP: + showZoomControlsWithTimeout(); + break; + } + } + } + + void onZoomLevelChange(int zoomLevel) { + boolean zoomInEnabled = zoomLevel < mZoomLevelMax; + boolean zoomOutEnabled = zoomLevel > mZoomLevelMin; + + mZoomControls.setIsZoomInEnabled(zoomInEnabled); + mZoomControls.setIsZoomOutEnabled(zoomOutEnabled); + } +} diff --git a/src/org/mapsforge/android/Projection.java b/src/org/mapsforge/android/Projection.java new file mode 100644 index 00000000..578cd40c --- /dev/null +++ b/src/org/mapsforge/android/Projection.java @@ -0,0 +1,91 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android; + +import org.mapsforge.core.GeoPoint; + +import android.graphics.Point; + +/** + * A Projection translates between the pixel coordinate system on the screen and geographical points on the earth. To + * retrieve the currently used Projection for a given MapView, call the {@link MapView#getProjection()} method. + */ +public interface Projection { + /** + * Translates the given screen coordinates to a {@link GeoPoint}. If the corresponding MapView has no valid + * dimensions (width and height > 0), null is returned. + * + * @param x + * the pixel x coordinate on the screen. + * @param y + * the pixel y coordinate on the screen. + * @return a new {@link GeoPoint} or null, if the corresponding MapView has no valid dimensions. + */ + GeoPoint fromPixels(int x, int y); + + /** + * @return the latitude span from the top to the bottom of the map in microdegrees (degrees * 10^6). + * @throws IllegalStateException + * if the MapView dimensions are not valid (width and height > 0). + */ + int getLatitudeSpan(); + + /** + * @return the longitude span from the left to the right of the map in microdegrees (degrees * 10^6). + * @throws IllegalStateException + * if the MapView dimensions are not valid (width and height > 0). + */ + int getLongitudeSpan(); + + /** + * Converts the given distance in meters at the given zoom level to the corresponding number of horizontal pixels. + * The calculation is carried out at the current latitude coordinate. + * + * @param meters + * the distance in meters. + * @param zoomLevel + * the zoom level at which the distance should be calculated. + * @return the number of pixels at the current map position and the given zoom level. + */ + float metersToPixels(float meters, byte zoomLevel); + + /** + * Translates the given {@link GeoPoint} to relative pixel coordinates on the screen. If the corresponding MapView + * has no valid dimensions (width and height > 0), null is returned. + * + * @param in + * the geographical point to convert. + * @param out + * an already existing object to use for the output. If this parameter is null, a new Point object will + * be created and returned. + * @return a Point which is relative to the top-left of the MapView or null, if the corresponding MapView has no + * valid dimensions. + */ + Point toPixels(GeoPoint in, Point out); + + /** + * Translates the given {@link GeoPoint} to absolute pixel coordinates on the world map. + * + * @param in + * the geographical point to convert. + * @param out + * an already existing object to use for the output. If this parameter is null, a new Point object will + * be created and returned. + * @param zoomLevel + * the zoom level at which the point should be converted. + * @return a Point which is relative to the top-left of the world map. + */ + Point toPoint(GeoPoint in, Point out, byte zoomLevel); +} diff --git a/src/org/mapsforge/android/glrenderer/DatabaseRenderer.java b/src/org/mapsforge/android/glrenderer/DatabaseRenderer.java new file mode 100644 index 00000000..c17ac389 --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/DatabaseRenderer.java @@ -0,0 +1,491 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.glrenderer; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +import javax.xml.parsers.ParserConfigurationException; + +import org.mapsforge.android.MapView; +import org.mapsforge.android.mapgenerator.JobTheme; +import org.mapsforge.android.mapgenerator.MapGenerator; +import org.mapsforge.android.mapgenerator.MapGeneratorJob; +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderTheme; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.android.rendertheme.renderinstruction.Area; +import org.mapsforge.android.rendertheme.renderinstruction.Line; +import org.mapsforge.core.GeoPoint; +import org.mapsforge.core.SphericalMercator; +import org.mapsforge.core.Tag; +import org.mapsforge.core.Tile; +import org.mapsforge.mapdatabase.IMapDatabase; +import org.mapsforge.mapdatabase.IMapDatabaseCallback; +import org.mapsforge.mapdatabase.MapFileInfo; +import org.xml.sax.SAXException; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.Log; + +/** + * + */ +public class DatabaseRenderer implements MapGenerator, RenderCallback, IMapDatabaseCallback { + private static String TAG = DatabaseRenderer.class.getName(); + + private static final byte ZOOM_MAX = 22; + private static final Byte DEFAULT_START_ZOOM_LEVEL = Byte.valueOf((byte) 16); + private static final double PI180 = (Math.PI / 180) / 1000000.0; + private static final double PIx4 = Math.PI * 4; + private static final double STROKE_INCREASE = 1.5; + private static final byte STROKE_MIN_ZOOM_LEVEL = 12; + private static final byte LAYERS = 11; + + private static RenderTheme renderTheme; + + private IMapDatabase mMapDatabase; + + private JobTheme mPreviousJobTheme; + // private float mPreviousTextScale; + private byte mPreviousZoomLevel; + + private GLMapTile mCurrentTile; + + private float[] mWayNodes; + private int[] mWays; + + private ArrayList mCurrentLines; + + private LineLayers mLineLayers; + private PolygonLayers mPolyLayers; + + private int mDrawingLayer; + private int mLevels; + + /** + * + */ + public DatabaseRenderer() { + Log.d(TAG, "init DatabaseRenderer"); + mCurrentLines = new ArrayList(); + + LayerPool.init(); + } + + @Override + public void renderPointOfInterest(byte layer, int latitude, int longitude, Tag[] tags) { + // TODO Auto-generated method stub + + } + + @Override + public void renderWaterBackground() { + // TODO Auto-generated method stub + + } + + private boolean mProjected; + private boolean mProjectedResult; + private float mSimplify; + private static final double f900913 = 20037508.342789244; + + private boolean projectToTile(boolean area) { + if (mProjected) + return mProjectedResult; + + float minx = Float.MAX_VALUE, miny = Float.MAX_VALUE, maxx = Float.MIN_VALUE, maxy = Float.MIN_VALUE; + + float[] coords = mWayNodes; + + long x = mCurrentTile.x; + long y = mCurrentTile.y; + long z = Tile.TILE_SIZE << mCurrentTile.zoomLevel; + float min = mSimplify; + + double divx, divy; + long dx = (x - (z >> 1)); + long dy = (y - (z >> 1)); + + if (!useSphericalMercator) { + divx = 180000000.0 / (z >> 1); + divy = z / PIx4; + } else { + divx = f900913 / (z >> 1); + divy = f900913 / (z >> 1); + } + + for (int pos = 0, outPos = 0, i = 0, m = mWays.length; i < m; i++) { + int len = mWays[i]; + int cnt = 0; + float lat, lon, prevLon = 0, prevLat = 0; + + for (int end = pos + len; pos < end; pos += 2) { + + if (useSphericalMercator) { + lon = (float) (coords[pos] / divx - dx); + lat = (float) (coords[pos + 1] / divy + dy); + } else { + lon = (float) ((coords[pos]) / divx - dx); + double sinLat = Math.sin(coords[pos + 1] * PI180); + lat = (float) (Math.log((1.0 + sinLat) / (1.0 - sinLat)) * divy + dy); + } + + if (area && i == 0) { + if (lon < minx) + minx = lon; + if (lon > maxx) + maxx = lon; + if (lat < miny) + miny = lat; + if (lat > maxy) + maxy = lat; + } + + if (cnt != 0) { + // drop small distance intermediate nodes + + if (lat == prevLat && lon == prevLon) + continue; + + if ((pos != end - 2) && + !((lat > prevLat + min || lat < prevLat - min) || + (lon > prevLon + min || lon < prevLon - min))) + continue; + } + coords[outPos++] = prevLon = lon; + coords[outPos++] = prevLat = lat; + + cnt += 2; + } + + if (area) { + // Log.d(TAG, "area:" + (maxx - minx) * (maxy - miny)); + if ((maxx - minx) * (maxy - miny) < 2000 / mCurrentTile.zoomLevel) { + mProjected = true; + mProjectedResult = false; + return false; + } + } + + mWays[i] = cnt; + } + mProjected = true; + mProjectedResult = true; + return true; + } + + private boolean firstMatch; + private boolean prevClosed; + + @Override + public void renderWay(byte layer, Tag[] tags, float[] wayNodes, int[] wayLength, boolean changed) { + + mProjected = false; + mDrawingLayer = getValidLayer(layer) * mLevels; + + int len = wayLength[0]; + boolean closed = (wayNodes[0] == wayNodes[len - 2] && + wayNodes[1] == wayNodes[len - 1]); + + mSimplify = 2.5f; + + if (closed) { + if (mCurrentTile.zoomLevel < 14) + mSimplify = 1.5f; + else + mSimplify = 0.5f; + + if (tags.length == 1 && "water".equals(tags[0].value)) + mSimplify = 0; + } + + mCurrentLines.clear(); + mWayNodes = wayNodes; + mWays = wayLength; + + if (!firstMatch && prevClosed == closed && !changed) { + DatabaseRenderer.renderTheme.matchWay(this, tags, mCurrentTile.zoomLevel, closed, false); + } else { + prevClosed = closed; + DatabaseRenderer.renderTheme.matchWay(this, tags, mCurrentTile.zoomLevel, closed, true); + } + + firstMatch = false; + } + + @Override + public void renderAreaCaption(String caption, float verticalOffset, Paint paint, Paint stroke) { + // TODO Auto-generated method stub + + } + + @Override + public void renderAreaSymbol(Bitmap symbol) { + // TODO Auto-generated method stub + + } + + @Override + public void renderPointOfInterestCaption(String caption, float verticalOffset, Paint paint, Paint stroke) { + // TODO Auto-generated method stub + + } + + @Override + public void renderPointOfInterestCircle(float radius, Paint fill, int level) { + // TODO Auto-generated method stub + + } + + @Override + public void renderPointOfInterestSymbol(Bitmap symbol) { + // TODO Auto-generated method stub + + } + + @Override + public void renderWay(Line line) { + + projectToTile(false); + + LineLayer outlineLayer = null; + LineLayer l = mLineLayers.getLayer(mDrawingLayer + line.level, line.color, false, line.fixed); + + float w = line.strokeWidth; + + if (!line.fixed) + w *= mStrokeScale / 1.5f; + + if (line.outline != -1) { + Line outline = DatabaseRenderer.renderTheme.getOutline(line.outline); + if (outline != null) { + outlineLayer = mLineLayers.getLayer(mDrawingLayer + outline.level, outline.color, true, false); + outlineLayer.addOutline(l); + } + } + + for (int i = 0, pos = 0, n = mWays.length; i < n; i++) { + int length = mWays[i]; + + // need at least two points + if (length >= 4) + l.addLine(mWayNodes, pos, length, w, line.round); + + pos += length; + } + } + + @Override + public void renderArea(Area area) { + if (!mDebugDrawPolygons) + return; + + // if (!projectToTile(mCurrentTile.zoomLevel < 13)) + if (!projectToTile(false)) + return; + + PolygonLayer l = mPolyLayers.getLayer(mDrawingLayer + area.level, area.color, area.fade); + + for (int i = 0, pos = 0, n = mWays.length; i < n; i++) { + int length = mWays[i]; + // need at least three points + if (length >= 6) + l.addPolygon(mWayNodes, pos, length); + + pos += length; + } + } + + @Override + public void renderWaySymbol(Bitmap symbol, boolean alignCenter, boolean repeat) { + // TODO Auto-generated method stub + + } + + @Override + public void renderWayText(String text, Paint paint, Paint stroke) { + // TODO Auto-generated method stub + + } + + @Override + public void cleanup() { + // TODO Auto-generated method stub + + } + + private boolean mDebugDrawPolygons; + + @Override + public boolean executeJob(MapGeneratorJob mapGeneratorJob) { + // Log.d(TAG, "load " + mCurrentTile); + + if (!(mapGeneratorJob.tile instanceof GLMapTile)) + return false; + + if (mMapDatabase == null) + return false; + + mCurrentTile = (GLMapTile) mapGeneratorJob.tile; + mDebugDrawPolygons = !mapGeneratorJob.debugSettings.mDisablePolygons; + + // FIXME still chance of concurrency with maprenderer updateVisibleList ? + if (mCurrentTile.isLoading || mCurrentTile.isDrawn) + return false; + + mCurrentTile.isLoading = true; + + JobTheme jobTheme = mapGeneratorJob.jobParameters.jobTheme; + + if (jobTheme != mPreviousJobTheme) { + if (!setRenderTheme(jobTheme)) { + mPreviousJobTheme = null; + return false; + } + + mPreviousJobTheme = jobTheme; + mPreviousZoomLevel = Byte.MIN_VALUE; + mLevels = DatabaseRenderer.renderTheme.getLevels(); + } + + byte zoomLevel = mCurrentTile.zoomLevel; + if (zoomLevel != mPreviousZoomLevel) { + setScaleStrokeWidth(zoomLevel); + mPreviousZoomLevel = zoomLevel; + } + + mLineLayers = new LineLayers(); + mPolyLayers = new PolygonLayers(); + mCurrentTile.lineLayers = mLineLayers; + mCurrentTile.polygonLayers = mPolyLayers; + + firstMatch = true; + mMapDatabase.executeQuery(mCurrentTile, this); + + // Log.d(TAG, "loaded " + mCurrentTile); + + if (mapGeneratorJob.debugSettings.mDrawTileFrames) { + float[] coords = { 0, 0, 0, Tile.TILE_SIZE, Tile.TILE_SIZE, Tile.TILE_SIZE, Tile.TILE_SIZE, 0, 0, 0 }; + LineLayer ll = mLineLayers.getLayer(Integer.MAX_VALUE, Color.BLACK, false, true); + ll.addLine(coords, 0, coords.length, 1.0f, false); + } + + mCurrentTile.newData = true; + return true; + } + + @Override + public GeoPoint getStartPoint() { + useSphericalMercator = false; + + if (mMapDatabase != null && mMapDatabase.hasOpenFile()) { + MapFileInfo mapFileInfo = mMapDatabase.getMapFileInfo(); + + if (SphericalMercator.NAME.equals(mapFileInfo.projectionName)) { + Log.d(TAG, "using Spherical Mercator"); + + useSphericalMercator = true; + } + if (mapFileInfo.startPosition != null) { + return mapFileInfo.startPosition; + } else if (mapFileInfo.mapCenter != null) { + return mapFileInfo.mapCenter; + } + + } + return null; + } + + @Override + public Byte getStartZoomLevel() { + return DEFAULT_START_ZOOM_LEVEL; + } + + @Override + public byte getZoomLevelMax() { + return ZOOM_MAX; + } + + @Override + public boolean requiresInternetConnection() { + return false; + } + + private static boolean setRenderTheme(JobTheme jobTheme) { + InputStream inputStream = null; + try { + inputStream = jobTheme.getRenderThemeAsStream(); + DatabaseRenderer.renderTheme = RenderThemeHandler.getRenderTheme(inputStream); + return true; + } catch (ParserConfigurationException e) { + Log.e(TAG, e.getMessage()); + } catch (SAXException e) { + Log.e(TAG, e.getMessage()); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + } + } + return false; + } + + private static byte getValidLayer(byte layer) { + if (layer < 0) { + return 0; + } else if (layer >= LAYERS) { + return LAYERS - 1; + } else { + return layer; + } + } + + @Override + public MapRenderer getMapRenderer(MapView mapView) { + return new MapRenderer(mapView); + } + + private boolean useSphericalMercator = false; + + @Override + public void setMapDatabase(IMapDatabase mapDatabase) { + mMapDatabase = mapDatabase; + } + + private static float mStrokeScale = 1.0f; + + /** + * Sets the scale stroke factor for the given zoom level. + * + * @param zoomLevel + * the zoom level for which the scale stroke factor should be set. + */ + private static void setScaleStrokeWidth(byte zoomLevel) { + int zoomLevelDiff = Math.max(zoomLevel - STROKE_MIN_ZOOM_LEVEL, 0); + mStrokeScale = (float) Math.pow(STROKE_INCREASE, zoomLevelDiff); + if (mStrokeScale < 1) + mStrokeScale = 1; + // DatabaseRenderer.renderTheme.scaleStrokeWidth(mStrokeScale); + } +} diff --git a/src/org/mapsforge/android/glrenderer/GLMapTile.java b/src/org/mapsforge/android/glrenderer/GLMapTile.java new file mode 100644 index 00000000..223e912f --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/GLMapTile.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ +package org.mapsforge.android.glrenderer; + +import org.mapsforge.android.mapgenerator.MapTile; +import org.mapsforge.core.Tile; + +class GLMapTile extends MapTile { + + VertexBufferObject lineVBO; + VertexBufferObject polygonVBO; + + LineLayers lineLayers; + PolygonLayers polygonLayers; + + boolean newData; + boolean loading; + + // pixel coordinates (y-flipped) + final long x; + final long y; + + // scissor coordinates + int sx, sy, sw, sh; + + final GLMapTile[] child = { null, null, null, null }; + GLMapTile parent; + + GLMapTile(long tileX, long tileY, byte zoomLevel) { + super(tileX, tileY, zoomLevel); + + x = pixelX; + y = pixelY + Tile.TILE_SIZE; + } + +} diff --git a/src/org/mapsforge/android/glrenderer/Layer.java b/src/org/mapsforge/android/glrenderer/Layer.java new file mode 100644 index 00000000..45bb9973 --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/Layer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ +package org.mapsforge.android.glrenderer; + +import java.util.LinkedList; + +class Layer { + LinkedList pool; + protected PoolItem curItem; + + int verticesCnt; + int offset; + + final int layer; + final int color; + + Layer(int l, int c) { + color = c; + layer = l; + verticesCnt = 0; + } + + float[] getNextItem() { + curItem.used = PoolItem.SIZE; + curItem = LayerPool.get(); + pool.add(curItem); + return curItem.vertices; + } +} diff --git a/src/org/mapsforge/android/glrenderer/LayerPool.java b/src/org/mapsforge/android/glrenderer/LayerPool.java new file mode 100644 index 00000000..9621c0ee --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/LayerPool.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ + +package org.mapsforge.android.glrenderer; + +import java.util.LinkedList; + +class LayerPool { + static private LinkedList pool; + static private int count; + + static void init() { + pool = new LinkedList(); + count = 0; + } + + static PoolItem get() { + if (count == 0) + return new PoolItem(); + + PoolItem it; + synchronized (pool) { + count--; + it = pool.pop(); + it.used = 0; + } + + return it; + } + + static void add(LinkedList items) { + int size = items.size(); + synchronized (pool) { + while (count < 4096 && size-- > 0) { + count++; + pool.add(items.pop()); + } + } + } +} diff --git a/src/org/mapsforge/android/glrenderer/LineLayer.java b/src/org/mapsforge/android/glrenderer/LineLayer.java new file mode 100644 index 00000000..7221d673 --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/LineLayer.java @@ -0,0 +1,397 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ +package org.mapsforge.android.glrenderer; + +import java.util.ArrayList; +import java.util.LinkedList; + +import org.mapsforge.core.Tile; + +class LineLayer extends Layer { + ArrayList outlines; + float[] colors; + boolean isOutline; + boolean isFixed; + float width; + + LineLayer(int layer, int color, boolean outline, boolean fixed) { + super(layer, color); + isOutline = outline; + isFixed = fixed; + if (outline) { + outlines = new ArrayList(); + } else { + curItem = LayerPool.get(); + + pool = new LinkedList(); + pool.add(curItem); + } + + colors = new float[4]; + + float a = (color >> 24 & 0xff) / 255.0f; + + colors[0] = (color >> 16 & 0xff) / 255.0f * a; + colors[1] = (color >> 8 & 0xff) / 255.0f * a; + colors[2] = (color >> 0 & 0xff) / 255.0f * a; + colors[3] = a; + } + + void addOutline(LineLayer link) { + if (!outlines.contains(link)) + outlines.add(link); + } + + // private void addVertex(float x, float y, byte tex, byte[] c){ + // // + // } + + void addLine(float[] pointArray, int pos, int length, float w, boolean capRound) { + float x, y, nextX, nextY, prevX, prevY, ux, uy, vx, vy, wx, wy; + double a; + int pointPos = pos; + boolean rounded = capRound; + width = w; + if (w < 0.5) + rounded = false; + + // amount of vertices used + verticesCnt += length + (rounded ? 6 : 2); + + float[] curVertices = curItem.vertices; + int vertexPos = curItem.used; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + x = pointArray[pointPos++]; + y = pointArray[pointPos++]; + + nextX = pointArray[pointPos++]; + nextY = pointArray[pointPos++]; + + // Calculate triangle corners for the given width + vx = nextX - x; + vy = nextY - y; + + a = Math.sqrt(vx * vx + vy * vy); + + vx = (float) (vx / a); + vy = (float) (vy / a); + + ux = -vy; + uy = vx; + + float uxw = ux * w; + float uyw = uy * w; + + float vxw = vx * w; + float vyw = vy * w; + + boolean outside = (x <= 0 || x >= Tile.TILE_SIZE || y <= 0 || y >= Tile.TILE_SIZE) + && (x - vxw <= 0 || x - vxw >= Tile.TILE_SIZE || y - vyw <= 0 || y - vyw >= Tile.TILE_SIZE); + + if (rounded && !outside) { + + // Add the first point twice to be able to draw with GL_TRIANGLE_STRIP + + curVertices[vertexPos++] = x + uxw - vxw; + curVertices[vertexPos++] = y + uyw - vyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = 1.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x + uxw - vxw; + curVertices[vertexPos++] = y + uyw - vyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = 1.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x - uxw - vxw; + curVertices[vertexPos++] = y - uyw - vyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = 1.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + // Start of line + curVertices[vertexPos++] = x + uxw; + curVertices[vertexPos++] = y + uyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = 0.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x - uxw; + curVertices[vertexPos++] = y - uyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = 0.0f; + + } else { + // outside means line is probably clipped + // TODO should align ending with tile boundary + // for now, just extend the line a little + if (!outside) { + vxw *= 0.5; + vyw *= 0.5; + } + if (rounded) { + verticesCnt -= 2; + } + // Add the first point twice to be able to draw with GL_TRIANGLE_STRIP + curVertices[vertexPos++] = x + uxw - vxw; + curVertices[vertexPos++] = y + uyw - vyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = 0.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x + uxw - vxw; + curVertices[vertexPos++] = y + uyw - vyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = 0.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x - uxw - vxw; + curVertices[vertexPos++] = y - uyw - vyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = 0.0f; + } + + prevX = x; + prevY = y; + x = nextX; + y = nextY; + // boolean flipped = false; + + for (; pointPos < pos + length;) { + nextX = pointArray[pointPos++]; + nextY = pointArray[pointPos++]; + + // Unit vector pointing back to previous node + vx = prevX - x; + vy = prevY - y; + a = Math.sqrt(vx * vx + vy * vy); + vx = (float) (vx / a); + vy = (float) (vy / a); + + // Unit vector pointing forward to next node + wx = nextX - x; + wy = nextY - y; + a = Math.sqrt(wx * wx + wy * wy); + wx = (float) (wx / a); + wy = (float) (wy / a); + + // Sum of these two vectors points + ux = vx + wx; + uy = vy + wy; + a = -wy * ux + wx * uy; + + if ((a < 0.1 && a > -0.1)) { + // Almost straight, use normal vector + ux = -wy; + uy = wx; + } else { + ux = (float) (ux / a); + uy = (float) (uy / a); + + if (ux > 2 || uy > 2 || ux < -2 || uy < -2) { + ux = -wy; + uy = wx; + + // ux = vx + wx; + // uy = vy + wy; + // // Normalize u, and project normal vector onto this + // double c = Math.sqrt(ux * ux + uy * uy); + // if (a < 0) { + // ux = (float) -(ux / c); + // uy = (float) -(uy / c); + // } + // else { + // ux = (float) (ux / c); + // uy = (float) (uy / c); + // } + // flipped = flipped ? false : true; + } + } + + uxw = ux * w; + uyw = uy * w; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x + uxw; + curVertices[vertexPos++] = y + uyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = 0.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x - uxw; + curVertices[vertexPos++] = y - uyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = 0.0f; + + prevX = x; + prevY = y; + x = nextX; + y = nextY; + } + + vx = prevX - x; + vy = prevY - y; + + a = Math.sqrt(vx * vx + vy * vy); + + vx = (float) (vx / a); + vy = (float) (vy / a); + + ux = vy; + uy = -vx; + + uxw = ux * w; + uyw = uy * w; + + vxw = vx * w; + vyw = vy * w; + + outside = (x <= 0 || x >= Tile.TILE_SIZE || y <= 0 || y >= Tile.TILE_SIZE) + && (x - vxw <= 0 || x - vxw >= Tile.TILE_SIZE || y - vyw <= 0 || y - vyw >= Tile.TILE_SIZE); + + if (vertexPos == PoolItem.SIZE) { + curItem.used = vertexPos; + curItem = LayerPool.get(); + pool.add(curItem); + curVertices = curItem.vertices; + vertexPos = 0; + } + + if (rounded && !outside) { + curVertices[vertexPos++] = x + uxw; + curVertices[vertexPos++] = y + uyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = 0.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x - uxw; + curVertices[vertexPos++] = y - uyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = 0.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + // For rounded line edges + curVertices[vertexPos++] = x + uxw - vxw; + curVertices[vertexPos++] = y + uyw - vyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = -1.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + // Add the last vertex twice to be able to draw with GL_TRIANGLE_STRIP + curVertices[vertexPos++] = x - uxw - vxw; + curVertices[vertexPos++] = y - uyw - vyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = -1.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x - uxw - vxw; + curVertices[vertexPos++] = y - uyw - vyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = -1.0f; + + } else { + if (!outside) { + vxw *= 0.5; + vyw *= 0.5; + } + if (rounded) { + verticesCnt -= 2; + } + + curVertices[vertexPos++] = x + uxw; + curVertices[vertexPos++] = y + uyw; + curVertices[vertexPos++] = -1.0f; + curVertices[vertexPos++] = 0.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + // Add the last vertex twice to be able to draw with GL_TRIANGLE_STRIP + curVertices[vertexPos++] = x - uxw - vxw; + curVertices[vertexPos++] = y - uyw - vyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = 0.0f; + + if (vertexPos == PoolItem.SIZE) { + curVertices = getNextItem(); + vertexPos = 0; + } + + curVertices[vertexPos++] = x - uxw - vxw; + curVertices[vertexPos++] = y - uyw - vyw; + curVertices[vertexPos++] = 1.0f; + curVertices[vertexPos++] = 0.0f; + + } + + curItem.used = vertexPos; + } +} diff --git a/src/org/mapsforge/android/glrenderer/LineLayers.java b/src/org/mapsforge/android/glrenderer/LineLayers.java new file mode 100644 index 00000000..e4017e3e --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/LineLayers.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ +package org.mapsforge.android.glrenderer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import android.util.SparseArray; + +class LineLayers { + private static int NUM_VERTEX_FLOATS = 4; + + private SparseArray layers; + + LineLayer[] array = null; + int size = 0; + + LineLayers() { + layers = new SparseArray(10); + } + + LineLayer getLayer(int layer, int color, boolean outline, boolean fixed) { + LineLayer l = layers.get(layer); + if (l != null) { + return l; + } + + l = new LineLayer(layer, color, outline, fixed); + layers.put(layer, l); + + return l; + } + + FloatBuffer compileLayerData(FloatBuffer buf) { + FloatBuffer fbuf = buf; + + array = new LineLayer[layers.size()]; + + for (int i = 0, n = layers.size(); i < n; i++) { + LineLayer l = layers.valueAt(i); + array[i] = l; + size += l.verticesCnt * NUM_VERTEX_FLOATS; + } + + if (buf == null || buf.capacity() < size) { + ByteBuffer bbuf = ByteBuffer.allocateDirect(size * 4).order(ByteOrder.nativeOrder()); + fbuf = bbuf.asFloatBuffer(); + } else { + fbuf.position(0); + } + int pos = 0; + + for (int i = 0, n = array.length; i < n; i++) { + LineLayer l = array[i]; + if (l.isOutline) + continue; + + for (PoolItem item : l.pool) { + fbuf.put(item.vertices, 0, item.used); + } + + l.offset = pos; + pos += l.verticesCnt; + + LayerPool.add(l.pool); + l.pool = null; + } + + fbuf.position(0); + + // not needed for drawing + layers = null; + + return fbuf; + } + + ByteBuffer compileLayerData(ByteBuffer buf) { + ByteBuffer sbuf = buf; + + array = new LineLayer[layers.size()]; + + for (int i = 0, n = layers.size(); i < n; i++) { + LineLayer l = layers.valueAt(i); + array[i] = l; + size += l.verticesCnt * NUM_VERTEX_FLOATS; + } + + if (buf == null || buf.capacity() < size * 2) { + sbuf = ByteBuffer.allocateDirect(size * 2).order(ByteOrder.nativeOrder()); + } else { + sbuf.position(0); + } + int pos = 0; + + byte[] data = new byte[PoolItem.SIZE * 2]; + + for (int i = 0, n = array.length; i < n; i++) { + LineLayer l = array[i]; + if (l.isOutline) + continue; + + for (int k = 0, m = l.pool.size(); k < m; k++) { + PoolItem item = l.pool.get(k); + PoolItem.toHalfFloat(item, data); + sbuf.put(data, 0, item.used * 2); + } + + l.offset = pos; + pos += l.verticesCnt; + + LayerPool.add(l.pool); + l.pool = null; + } + + sbuf.position(0); + + // not needed for drawing + layers = null; + + return sbuf; + } +} diff --git a/src/org/mapsforge/android/glrenderer/MapRenderer.java b/src/org/mapsforge/android/glrenderer/MapRenderer.java new file mode 100644 index 00000000..f91f9a74 --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/MapRenderer.java @@ -0,0 +1,1057 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ +package org.mapsforge.android.glrenderer; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import org.mapsforge.android.DebugSettings; +import org.mapsforge.android.MapView; +import org.mapsforge.android.mapgenerator.JobParameters; +import org.mapsforge.android.mapgenerator.MapGenerator; +import org.mapsforge.android.mapgenerator.MapGeneratorJob; +import org.mapsforge.android.mapgenerator.MapWorker; +import org.mapsforge.android.mapgenerator.TileCacheKey; +import org.mapsforge.android.mapgenerator.TileDistanceSort; +import org.mapsforge.android.utils.GlConfigChooser; +import org.mapsforge.android.utils.GlUtils; +import org.mapsforge.core.MapPosition; +import org.mapsforge.core.MercatorProjection; +import org.mapsforge.core.Tile; + +import android.opengl.GLES20; +import android.opengl.Matrix; +import android.os.SystemClock; +import android.util.FloatMath; +import android.util.Log; + +/** + * TODO - use proxy child/parent tile nearer to current tile (currently it is always parent first) - use stencil instead + * of scissor mask for rotation - draw up to two parents above current tile, maybe prefetch parent + */ +public class MapRenderer implements org.mapsforge.android.MapRenderer { + private static final String TAG = "MapRenderer"; + + private static final int CACHE_TILES = 250; + private static final int LIMIT_BUFFERS = 32 * (1024 * 1024); + + private static final int OES_HALF_FLOAT = 0x8D61; + private static final int FLOAT_BYTES = 4; + private static final int SHORT_BYTES = 2; + private static final int POLYGON_VERTICES_DATA_POS_OFFSET = 0; + private static final int LINE_VERTICES_DATA_POS_OFFSET = 0; + private static final int LINE_VERTICES_DATA_TEX_OFFSET = 8; + + private static int STENCIL_BITS = 8; + + private final MapView mMapView; + private final MapWorker mMapWorker; + + private final ArrayList mJobList; + private final ArrayList mVBOs; + private final TileCacheKey mTileCacheKey; + private final HashMap mTiles; + private final ArrayList mTileList; + private final TileDistanceSort mTileDistanceSort; + + private DebugSettings mDebugSettings; + private JobParameters mJobParameter; + private MapPosition mMapPosition, mPrevMapPosition; + + private int mWidth, mHeight; + private float mAspect; + + // draw position is updated from current position in onDrawFrame + // keeping the position consistent while drawing + private double mDrawX, mDrawY, mDrawZ, mCurX, mCurY, mCurZ; + private float mDrawScale, mCurScale; + + // current center tile + private long mTileX, mTileY; + + private FloatBuffer floatBuffer = null; + private ByteBuffer byteBuffer = null; + + boolean useHalfFloat = false; + + // bytes currently loaded in VBOs + private int mBufferMemoryUsage; + + // flag set by updateVisibleList when current visible tiles changed. + // used in onDrawFrame to nextTiles to curTiles + private boolean mUpdateTiles; + + class TilesData { + int cnt = 0; + final GLMapTile[] tiles; + + TilesData(int numTiles) { + tiles = new GLMapTile[numTiles]; + } + } + + private float[] mMVPMatrix = new float[16]; + // private float[] mMMatrix = new float[16]; + // private float[] mRMatrix = new float[16]; + + // newTiles is set in updateVisibleList and synchronized swapped + // with nextTiles on main thread. + // nextTiles is swapped with curTiles in onDrawFrame in GL thread. + private TilesData newTiles, nextTiles, curTiles; + + private boolean mInitial; + + // shader handles + private int gLineProgram; + private int gLineVertexPositionHandle; + private int gLineTexturePositionHandle; + private int gLineColorHandle; + private int gLineMatrixHandle; + private int gLineWidthHandle; + private int gLineModeHandle; + + private int gPolygonProgram; + private int gPolygonVertexPositionHandle; + private int gPolygonMatrixHandle; + private int gPolygonColorHandle; + + /** + * + */ + public boolean timing = false; + + /** + * @param mapView + * the MapView + */ + public MapRenderer(MapView mapView) { + Log.d(TAG, "init MapRenderer"); + mMapView = mapView; + mMapWorker = mapView.getMapWorker(); + mDebugSettings = mapView.getDebugSettings(); + + mVBOs = new ArrayList(); + mJobList = new ArrayList(); + + mTiles = new HashMap(CACHE_TILES * 2); + mTileList = new ArrayList(); + mTileCacheKey = new TileCacheKey(); + + mTileDistanceSort = new TileDistanceSort(); + Matrix.setIdentityM(mMVPMatrix, 0); + mInitial = true; + mUpdateTiles = false; + } + + private void updateTileDistances() { + byte zoom = mMapPosition.zoomLevel; + long x = mTileX; + long y = mTileY; + int diff; + long dx, dy; + for (int i = 0, n = mTileList.size(); i < n; i++) { + GLMapTile t = mTileList.get(i); + diff = (t.zoomLevel - zoom); + + if (diff != 0) { + if (diff > 0) { + dx = (t.tileX << diff) - x; + dy = (t.tileY << diff) - y; + } else { + dx = (t.tileX >> -diff) - x; + dy = (t.tileY >> -diff) - y; + } + + t.distance = ((dx > 0 ? dx : -dx) + (dy > 0 ? dy : -dy)); + t.distance *= (1 + t.zoomLevel); + } else { + dx = t.tileX - x; + dy = t.tileY - y; + t.distance = ((dx > 0 ? dx : -dx) + (dy > 0 ? dy : -dy)); + } + } + } + + private void limitCache(int remove) { + byte z = mMapPosition.zoomLevel; + + for (int j = mTileList.size() - 1, cnt = 0; cnt < remove && j > 0; j--, cnt++) { + + GLMapTile t = mTileList.remove(j); + if (t.isActive) { + // Log.d(TAG, "EEEK removing active tile"); + mTileList.add(t); + continue; + } + // check if this tile is used as proxy for not yet drawn active tile + if (t.isDrawn || t.newData || t.isLoading) { + if (t.zoomLevel == z + 1) { + if (t.parent != null && t.parent.isActive && !t.parent.isDrawn) { + mTileList.add(t); + // Log.d(TAG, "EEEK removing active proxy child"); + continue; + } + } else if (t.zoomLevel == z - 1) { + GLMapTile c = null; + for (int i = 0; i < 4; i++) { + c = t.child[i]; + if (c != null && c.isActive && !(c.isDrawn || c.newData)) + break; + c = null; + } + + if (c != null) { + // Log.d(TAG, "EEEK removing active proxy parent"); + mTileList.add(t); + continue; + } + } + } + + mTileCacheKey.set(t.tileX, t.tileY, t.zoomLevel); + mTiles.remove(mTileCacheKey); + + // clear references to this tile + for (int i = 0; i < 4; i++) { + if (t.child[i] != null) + t.child[i].parent = null; + } + + if (t.parent != null) { + for (int i = 0; i < 4; i++) { + if (t.parent.child[i] == t) { + t.parent.child[i] = null; + break; + } + } + } + + if (t.lineVBO != null) { + synchronized (mVBOs) { + mVBOs.add(t.lineVBO); + mVBOs.add(t.polygonVBO); + t.lineVBO = null; + t.polygonVBO = null; + } + } + } + } + + private boolean updateVisibleList(double x, double y) { + byte zoomLevel = mMapPosition.zoomLevel; + float scale = mMapPosition.scale; + double add = 1.0f / scale; + int offsetX = (int) ((mWidth >> 1) * add); + int offsetY = (int) ((mHeight >> 1) * add); + + long pixelRight = (long) x + offsetX; + long pixelBottom = (long) y + offsetY; + long pixelLeft = (long) x - offsetX; + long pixelTop = (long) y - offsetY; + + long tileLeft = MercatorProjection.pixelXToTileX(pixelLeft, zoomLevel); + long tileTop = MercatorProjection.pixelYToTileY(pixelTop, zoomLevel); + long tileRight = MercatorProjection.pixelXToTileX(pixelRight, zoomLevel); + long tileBottom = MercatorProjection.pixelYToTileY(pixelBottom, zoomLevel); + + mJobList.clear(); + mJobParameter = mMapView.getJobParameters(); + + MapGenerator mapGenerator = mMapView.getMapGenerator(); + int tiles = 0; + if (newTiles == null) + return false; + + int max = newTiles.tiles.length - 1; + + for (long tileY = tileTop - 1; tileY <= tileBottom + 1; tileY++) { + for (long tileX = tileLeft - 1; tileX <= tileRight + 1; tileX++) { + // FIXME + if (tiles == max) + break; + + GLMapTile tile = mTiles.get(mTileCacheKey.set(tileX, tileY, zoomLevel)); + + if (tile == null) { + tile = new GLMapTile(tileX, tileY, zoomLevel); + TileCacheKey key = new TileCacheKey(mTileCacheKey); + mTiles.put(key, tile); + mTileList.add(tile); + + mTileCacheKey.set((tileX >> 1), (tileY >> 1), (byte) (zoomLevel - 1)); + tile.parent = mTiles.get(mTileCacheKey); + + // set this tile to be child of its parent + if (tile.parent != null) { + int idx = (int) ((tileX & 0x01) + 2 * (tileY & 0x01)); + tile.parent.child[idx] = tile; + } + + long xx = tileX << 1; + long yy = tileY << 1; + byte z = (byte) (zoomLevel + 1); + + tile.child[0] = mTiles.get(mTileCacheKey.set(xx, yy, z)); + tile.child[1] = mTiles.get(mTileCacheKey.set(xx + 1, yy, z)); + tile.child[2] = mTiles.get(mTileCacheKey.set(xx, yy + 1, z)); + tile.child[3] = mTiles.get(mTileCacheKey.set(xx + 1, yy + 1, z)); + + // set this tile to be parent of its children + for (int i = 0; i < 4; i++) { + if (tile.child[i] != null) + tile.child[i].parent = tile; + } + + } + + newTiles.tiles[tiles++] = tile; + + if (!tile.isDrawn && !tile.isLoading) { + MapGeneratorJob job = new MapGeneratorJob(tile, mapGenerator, mJobParameter, mDebugSettings); + mJobList.add(job); + } + } + } + + updateTileDistances(); + + int removes = mTiles.size() - CACHE_TILES; + + if (removes > 0) + Collections.sort(mTileList, mTileDistanceSort); + + synchronized (this) { + for (int i = 0; i < nextTiles.cnt; i++) + nextTiles.tiles[i].isActive = false; + + for (int i = 0; i < curTiles.cnt; i++) + curTiles.tiles[i].isActive = true; + + for (int i = 0; i < tiles; i++) + newTiles.tiles[i].isActive = true; + + TilesData tmp = nextTiles; + nextTiles = newTiles; + nextTiles.cnt = tiles; + newTiles = tmp; + + mUpdateTiles = true; + } + + limitCache(removes); + + if (mJobList.size() > 0) { + mMapView.getJobQueue().setJobs(mJobList); + synchronized (mMapWorker) { + mMapWorker.notify(); + } + } + + return true; + } + + /** + * + */ + @Override + public synchronized void redrawTiles(boolean clear) { + boolean changedPos = false; + boolean changedZoom = false; + mMapPosition = mMapView.getMapPosition().getMapPosition(); + + if (mMapPosition == null) { + Log.d(TAG, ">>> no map position"); + return; + } + + if (clear) { + mInitial = true; + + for (GLMapTile t : mTileList) { + t.isDrawn = false; + t.isLoading = false; + t.newData = false; + } + } + + byte zoomLevel = mMapPosition.zoomLevel; + float scale = mMapPosition.scale; + + double x = MercatorProjection.longitudeToPixelX(mMapPosition.geoPoint.getLongitude(), zoomLevel); + double y = MercatorProjection.latitudeToPixelY(mMapPosition.geoPoint.getLatitude(), zoomLevel); + + long tileX = MercatorProjection.pixelXToTileX(x, zoomLevel); + long tileY = MercatorProjection.pixelYToTileY(y, zoomLevel); + + if (mInitial || mPrevMapPosition.zoomLevel != zoomLevel) + changedZoom = true; + else if (tileX != mTileX || tileY != mTileY) + changedPos = true; + + mInitial = false; + + mTileX = tileX; + mTileY = tileY; + mPrevMapPosition = mMapPosition; + + if (changedZoom) + updateVisibleList(x, y); + + synchronized (this) { + // do not change position while drawing + mCurX = x; + mCurY = y; + mCurZ = zoomLevel; + mCurScale = scale; + } + + if (!timing) + mMapView.requestRender(); + + if (changedPos) + updateVisibleList(x, y); + } + + @Override + public boolean passTile(MapGeneratorJob mapGeneratorJob) { + if (!timing) + mMapView.requestRender(); + return true; + } + + private void fillPolygons(int[] colors, int count) { + boolean blend = false; + + // draw to framebuffer + GLES20.glColorMask(true, true, true, true); + + // do not modify stencil buffer + GLES20.glStencilMask(0); + + for (int c = 0; c < count; c++) { + int color = colors[c]; + float alpha = (color >> 24 & 0xff) / 255f; + if (alpha != 1) { + if (!blend) { + GLES20.glEnable(GLES20.GL_BLEND); + blend = true; + } + } else if (blend) { + GLES20.glDisable(GLES20.GL_BLEND); + blend = false; + } + + GLES20.glUniform4f(gPolygonColorHandle, (color >> 16 & 0xff) / 255f * alpha, (color >> 8 & 0xff) / 255f + * alpha, (color & 0xff) / 255f * alpha, alpha); + + // set stencil buffer mask used to draw this layer + GLES20.glStencilFunc(GLES20.GL_EQUAL, 0xff, 1 << c); + + // draw tile fill coordinates + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + + if (blend) + GLES20.glDisable(GLES20.GL_BLEND); + } + + private int[] mPolyColors; + + private boolean drawPolygons(GLMapTile tile, int diff) { + float scale, x, y; + + if (tile.polygonLayers == null || tile.polygonLayers.array == null) + return true; + + GLES20.glScissor(tile.sx, tile.sy, tile.sw, tile.sh); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, tile.polygonVBO.id); + + if (useHalfFloat) { + GLES20.glVertexAttribPointer(gPolygonVertexPositionHandle, 2, OES_HALF_FLOAT, false, 0, + POLYGON_VERTICES_DATA_POS_OFFSET); + } else { + GLES20.glVertexAttribPointer(gPolygonVertexPositionHandle, 2, GLES20.GL_FLOAT, false, 0, + POLYGON_VERTICES_DATA_POS_OFFSET); + } + + if (diff == 0) { + scale = (float) (mDrawScale * 2.0 / mHeight); + x = (float) (mDrawX - tile.x); + y = (float) (tile.y - mDrawY); + } else { + float z = (diff > 0) ? (1 << diff) : 1.0f / (1 << -diff); + scale = (float) (mDrawScale * 2.0 / mHeight / z); + x = (float) (mDrawX * z - tile.x); + y = (float) (tile.y - mDrawY * z); + } + + int cnt = 0; + int[] colors = mPolyColors; + + mMVPMatrix[12] = -x * (scale * mAspect); + mMVPMatrix[13] = -y * (scale); + mMVPMatrix[0] = (scale * mAspect); + mMVPMatrix[5] = (scale); + + GLES20.glUniformMatrix4fv(gPolygonMatrixHandle, 1, false, mMVPMatrix, 0); + + boolean firstPass = true; + + for (int i = 0, n = tile.polygonLayers.array.length; i < n; i++) { + PolygonLayer l = tile.polygonLayers.array[i]; + + if (cnt == 0) { + // disable drawing to framebuffer + GLES20.glColorMask(false, false, false, false); + + // never pass the test, i.e. always apply first stencil op (sfail) + GLES20.glStencilFunc(GLES20.GL_NEVER, 0, 0xff); + + if (firstPass) + firstPass = false; + else { + // clear stencilbuffer + GLES20.glStencilMask(0xFF); + GLES20.glClear(GLES20.GL_STENCIL_BUFFER_BIT); + + // clear stencilbuffer (tile region) + // GLES20.glStencilOp(GLES20.GL_ZERO, GLES20.GL_ZERO, GLES20.GL_ZERO); + // GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + + // stencil op for stencil method polygon drawing + GLES20.glStencilOp(GLES20.GL_INVERT, GLES20.GL_INVERT, GLES20.GL_INVERT); + } + + colors[cnt] = l.color; + + // fade out polygon layers (set in RederTheme) + if (l.fadeLevel > 0) { + if (l.fadeLevel >= mDrawZ) { + + // skip layer when faded out + if (l.fadeLevel > mDrawZ) + continue; + + // modify alpha channel + float s = (mDrawScale < 1.3f ? 1.3f : mDrawScale); + colors[cnt] = (colors[cnt] & 0xffffff) | (byte) ((s - 1) * 0xff) << 24; + } + } + + // set stencil mask to draw to + GLES20.glStencilMask(1 << cnt++); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, l.offset, l.verticesCnt); + + // draw up to 8 layers into stencil buffer + if (cnt == STENCIL_BITS) { + fillPolygons(colors, cnt); + cnt = 0; + } + } + + if (cnt > 0) + fillPolygons(colors, cnt); + + return true; + } + + private boolean drawLines(GLMapTile tile, int diff) { + float x, y, scale; + float z = 1; + + if (tile.lineLayers == null || tile.lineLayers.array == null) + return false; + + GLES20.glScissor(tile.sx, tile.sy, tile.sw, tile.sh); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, tile.lineVBO.id); + + if (useHalfFloat) { + GLES20.glVertexAttribPointer(gLineVertexPositionHandle, 2, OES_HALF_FLOAT, false, 8, + LINE_VERTICES_DATA_POS_OFFSET); + + GLES20.glVertexAttribPointer(gLineTexturePositionHandle, 2, OES_HALF_FLOAT, false, 8, + LINE_VERTICES_DATA_TEX_OFFSET >> 1); + } else { + GLES20.glVertexAttribPointer(gLineVertexPositionHandle, 2, GLES20.GL_FLOAT, false, 16, + LINE_VERTICES_DATA_POS_OFFSET); + + GLES20.glVertexAttribPointer(gLineTexturePositionHandle, 2, GLES20.GL_FLOAT, false, 16, + LINE_VERTICES_DATA_TEX_OFFSET); + } + + if (diff == 0) { + scale = (float) (mDrawScale * 2.0 / mHeight); + x = (float) (mDrawX - tile.x); + y = (float) (tile.y - mDrawY); + } else { + z = (diff > 0) ? (1 << diff) : 1.0f / (1 << -diff); + scale = (float) (mDrawScale * 2.0 / mHeight / z); + x = (float) (mDrawX * z - tile.x); + y = (float) (tile.y - mDrawY * z); + } + + mMVPMatrix[12] = -x * (scale * mAspect); + mMVPMatrix[13] = -y * (scale); + mMVPMatrix[0] = (scale * mAspect); + mMVPMatrix[5] = (scale); + + GLES20.glUniformMatrix4fv(gLineMatrixHandle, 1, false, mMVPMatrix, 0); + + LineLayer[] layers = tile.lineLayers.array; + + boolean drawOutlines = false; + boolean drawFixed = false; + + // stroke scale factor (0.85 to give some room for anti-aliasing) + float wdiv = 0.85f / FloatMath.sqrt(mDrawScale / z); + // linear scale for fixed lines + float fdiv = 0.85f / (mDrawScale / z); + + for (int i = 0, n = layers.length; i < n; i++) { + LineLayer l = layers[i]; + + // set line width and mode + if (i == 0 | l.isOutline != drawOutlines || l.isFixed != drawFixed) { + drawOutlines = l.isOutline; + drawFixed = l.isFixed; + if (drawFixed) { + GLES20.glUniform1i(gLineModeHandle, 2); + GLES20.glUniform1f(gLineWidthHandle, fdiv); + } else if (drawOutlines) { + GLES20.glUniform1i(gLineModeHandle, 1); + GLES20.glUniform1f(gLineWidthHandle, wdiv); + } else { + GLES20.glUniform1i(gLineModeHandle, 0); + GLES20.glUniform1f(gLineWidthHandle, wdiv); + } + } + + GLES20.glUniform4fv(gLineColorHandle, 1, l.colors, 0); + + if (drawOutlines) { + for (int j = 0, m = l.outlines.size(); j < m; j++) { + LineLayer o = l.outlines.get(j); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, o.offset, o.verticesCnt); + } + } else { + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, l.offset, l.verticesCnt); + } + } + + return true; + } + + private boolean setTileScissor(GLMapTile tile, float div) { + + double dx, dy, scale; + + if (div == 0) { + dx = tile.pixelX - mDrawX; + dy = tile.pixelY - mDrawY; + scale = mDrawScale; + } else { + dx = tile.pixelX - mDrawX * div; + dy = tile.pixelY - mDrawY * div; + scale = mDrawScale / div; + } + + int sx = (int) (dx * scale); + int sy = (int) (dy * scale); + + int sw = (int) ((dx + Tile.TILE_SIZE) * scale) - sx; + int sh = (int) ((dy + Tile.TILE_SIZE) * scale) - sy; + + sx = (mWidth >> 1) + sx; + sy = (mHeight >> 1) - (sy + sh); + + // shrink width/height to screen intersection + if (sx < 0) { + sw += sx; + sx = 0; + } + + if (sy < 0) { + sh += sy; + sy = 0; + } + + if (sw + sx > mWidth) + sw = mWidth - sx; + + if (sh + tile.sy > mHeight) + sh = mHeight - sy; + + if (sw <= 0 || sh <= 0) { + tile.isVisible = false; + return false; + + } + + tile.isVisible = true; + tile.sx = sx; + tile.sy = sy; + tile.sw = sw; + tile.sh = sh; + + return true; + } + + private void drawProxyLines(GLMapTile tile) { + if (tile.parent != null && tile.parent.isDrawn) { + tile.parent.sx = tile.sx; + tile.parent.sy = tile.sy; + tile.parent.sw = tile.sw; + tile.parent.sh = tile.sh; + drawLines(tile.parent, -1); + } else { + // scissor coordinates already set for polygons + for (int i = 0; i < 4; i++) { + GLMapTile c = tile.child[i]; + if (c != null && c.isDrawn && c.isVisible) + drawLines(c, 1); + + } + } + } + + private void drawProxyPolygons(GLMapTile tile) { + if (tile.parent != null && tile.parent.isDrawn) { + tile.parent.sx = tile.sx; + tile.parent.sy = tile.sy; + tile.parent.sw = tile.sw; + tile.parent.sh = tile.sh; + drawPolygons(tile.parent, -1); + } else { + for (int i = 0; i < 4; i++) { + GLMapTile c = tile.child[i]; + if (c != null && c.isDrawn && setTileScissor(c, 2)) + drawPolygons(c, 1); + + } + } + } + + private boolean uploadTileData(GLMapTile tile) { + if (tile.lineVBO == null) { + // Upload line data to vertex buffer object + synchronized (mVBOs) { + if (mVBOs.size() < 2) + return false; + + tile.lineVBO = mVBOs.remove(mVBOs.size() - 1); + tile.polygonVBO = mVBOs.remove(mVBOs.size() - 1); + } + } + if (useHalfFloat) + byteBuffer = tile.lineLayers.compileLayerData(byteBuffer); + else + floatBuffer = tile.lineLayers.compileLayerData(floatBuffer); + + if (tile.lineLayers.size > 0) { + mBufferMemoryUsage -= tile.lineVBO.size; + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, tile.lineVBO.id); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, 0, null, GLES20.GL_STATIC_DRAW); + + if (useHalfFloat) { + tile.lineVBO.size = tile.lineLayers.size * SHORT_BYTES; + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, tile.lineVBO.size, byteBuffer, GLES20.GL_STATIC_DRAW); + } else { + tile.lineVBO.size = tile.lineLayers.size * FLOAT_BYTES; + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, tile.lineVBO.size, floatBuffer, GLES20.GL_STATIC_DRAW); + } + + mBufferMemoryUsage += tile.lineVBO.size; + + } else { + tile.lineLayers = null; + } + + if (useHalfFloat) + byteBuffer = tile.polygonLayers.compileLayerData(byteBuffer); + else + floatBuffer = tile.polygonLayers.compileLayerData(floatBuffer); + + // Upload polygon data to vertex buffer object + if (tile.polygonLayers.size > 0) { + mBufferMemoryUsage -= tile.polygonVBO.size; + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, tile.polygonVBO.id); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, 0, null, GLES20.GL_STATIC_DRAW); + + if (useHalfFloat) { + tile.polygonVBO.size = tile.polygonLayers.size * SHORT_BYTES; + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, tile.polygonVBO.size, byteBuffer, GLES20.GL_STATIC_DRAW); + } else { + tile.polygonVBO.size = tile.polygonLayers.size * FLOAT_BYTES; + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, tile.polygonVBO.size, floatBuffer, GLES20.GL_STATIC_DRAW); + } + mBufferMemoryUsage += tile.polygonVBO.size; + + } else { + tile.polygonLayers = null; + } + + tile.newData = false; + tile.isDrawn = true; + tile.isLoading = false; + + return true; + } + + @Override + public void onDrawFrame(GL10 glUnused) { + long start = 0, poly_time = 0, clear_time = 0; + + if (mMapPosition == null) + return; + + if (timing) + start = SystemClock.uptimeMillis(); + + GLES20.glStencilMask(0xFF); + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_STENCIL_BUFFER_BIT); + + synchronized (this) { + mDrawX = mCurX; + mDrawY = mCurY; + mDrawZ = mCurZ; + mDrawScale = mCurScale; + + if (mUpdateTiles) { + TilesData tmp = curTiles; + curTiles = nextTiles; + nextTiles = tmp; + mUpdateTiles = false; + } + } + + int tileCnt = curTiles.cnt; + GLMapTile[] tiles = curTiles.tiles; + + if (mBufferMemoryUsage > LIMIT_BUFFERS) { + Log.d(TAG, "buffer object usage: " + mBufferMemoryUsage / (1024 * 1024) + "MB"); + synchronized (mVBOs) { + for (VertexBufferObject vbo : mVBOs) { + if (vbo.size == 0) + continue; + mBufferMemoryUsage -= vbo.size; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo.id); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, 0, null, GLES20.GL_STATIC_DRAW); + vbo.size = 0; + + } + } + Log.d(TAG, " > " + mBufferMemoryUsage / (1024 * 1024) + "MB"); + } + + // check visible tiles, set tile clip scissors, upload new vertex data + for (int i = 0; i < tileCnt; i++) { + GLMapTile tile = tiles[i]; + + if (!setTileScissor(tile, 1)) + continue; + + if (tile.newData) { + uploadTileData(tile); + + if (timing) + Log.d(TAG, "buffer upload took: " + (SystemClock.uptimeMillis() - start)); + + continue; + } + + if (!tile.isDrawn) { + if (tile.parent != null) { + if (tile.parent.newData) + uploadTileData(tile.parent); + } else { + if (tile.child[0] != null && tile.child[0].newData) + uploadTileData(tile.child[0]); + if (tile.child[1] != null && tile.child[1].newData) + uploadTileData(tile.child[1]); + if (tile.child[2] != null && tile.child[2].newData) + uploadTileData(tile.child[2]); + if (tile.child[3] != null && tile.child[3].newData) + uploadTileData(tile.child[3]); + } + } + } + + if (timing) + clear_time = (SystemClock.uptimeMillis() - start); + + GLES20.glEnable(GLES20.GL_SCISSOR_TEST); + GLES20.glDisable(GLES20.GL_BLEND); + + // Draw Polygons + GLES20.glEnable(GLES20.GL_STENCIL_TEST); + + GLES20.glUseProgram(gPolygonProgram); + + for (int i = 0; i < tileCnt; i++) { + if (tiles[i].isVisible) { + GLMapTile tile = tiles[i]; + + if (tile.isDrawn) + drawPolygons(tile, 0); + else + drawProxyPolygons(tile); + } + } + + GLES20.glDisable(GLES20.GL_STENCIL_TEST); + + if (timing) { + GLES20.glFinish(); + poly_time = (SystemClock.uptimeMillis() - start); + } + + // Draw lines + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glUseProgram(gLineProgram); + + for (int i = 0; i < tileCnt; i++) { + if (tiles[i].isVisible) { + GLMapTile tile = tiles[i]; + + if (tile.isDrawn) + drawLines(tile, 0); + else + drawProxyLines(tile); + } + } + + // GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + if (timing) { + GLES20.glFinish(); + Log.d(TAG, "draw took " + (SystemClock.uptimeMillis() - start) + " " + clear_time + " " + poly_time); + } + + } + + private int[] mVboIds; + + @Override + public void onSurfaceChanged(GL10 glUnused, int width, int height) { + mVBOs.clear(); + mTiles.clear(); + mTileList.clear(); + + curTiles = newTiles = nextTiles = null; + mBufferMemoryUsage = 0; + + if (width <= 0 || height <= 0) + return; + + STENCIL_BITS = GlConfigChooser.stencilSize; + mPolyColors = new int[STENCIL_BITS]; + + mWidth = width; + mHeight = height; + mAspect = (float) height / width; + + GLES20.glViewport(0, 0, width, height); + + int tiles = (mWidth / Tile.TILE_SIZE + 4) * (mHeight / Tile.TILE_SIZE + 4); + curTiles = new TilesData(tiles); + newTiles = new TilesData(tiles); + nextTiles = new TilesData(tiles); + + // Set up vertex buffer objects + int numVBO = (CACHE_TILES + tiles) * 2; + mVboIds = new int[numVBO]; + GLES20.glGenBuffers(numVBO, mVboIds, 0); + + for (int i = 0; i < numVBO; i++) + mVBOs.add(new VertexBufferObject(mVboIds[i])); + + mDebugSettings = mMapView.getDebugSettings(); + mJobParameter = mMapView.getJobParameters(); + + mInitial = true; + mMapView.redrawTiles(); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + // Set up the program for rendering lines + + gLineProgram = GlUtils.createProgram(Shaders.gLineVertexShader, Shaders.gLineFragmentShader); + if (gLineProgram == 0) { + Log.e(TAG, "trying simple line program."); + gLineProgram = GlUtils.createProgram(Shaders.gLineVertexShader, Shaders.gLineFragmentShaderSimple); + if (gLineProgram == 0) { + Log.e(TAG, "Could not create line program."); + return; + } + } + + String ext = GLES20.glGetString(GLES20.GL_EXTENSIONS); + + if (ext.indexOf("GL_OES_vertex_half_float") >= 0) + useHalfFloat = true; + + Log.d(TAG, "Extensions: " + ext); + + gLineMatrixHandle = GLES20.glGetUniformLocation(gLineProgram, "u_center"); + gLineWidthHandle = GLES20.glGetUniformLocation(gLineProgram, "u_width"); + gLineModeHandle = GLES20.glGetUniformLocation(gLineProgram, "u_mode"); + gLineColorHandle = GLES20.glGetUniformLocation(gLineProgram, "u_color"); + gLineVertexPositionHandle = GLES20.glGetAttribLocation(gLineProgram, "a_position"); + gLineTexturePositionHandle = GLES20.glGetAttribLocation(gLineProgram, "a_st"); + + // Set up the program for rendering polygons + gPolygonProgram = GlUtils.createProgram(Shaders.gPolygonVertexShader, Shaders.gPolygonFragmentShader); + if (gPolygonProgram == 0) { + Log.e(TAG, "Could not create polygon program."); + return; + } + gPolygonMatrixHandle = GLES20.glGetUniformLocation(gPolygonProgram, "u_center"); + gPolygonVertexPositionHandle = GLES20.glGetAttribLocation(gPolygonProgram, "a_position"); + gPolygonColorHandle = GLES20.glGetUniformLocation(gPolygonProgram, "u_color"); + + GLES20.glUseProgram(gPolygonProgram); + GLES20.glEnableVertexAttribArray(gPolygonVertexPositionHandle); + + GLES20.glUseProgram(gLineProgram); + GLES20.glEnableVertexAttribArray(gLineVertexPositionHandle); + GLES20.glEnableVertexAttribArray(gLineTexturePositionHandle); + + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glDisable(GLES20.GL_DITHER); + GLES20.glClearColor(0.96f, 0.96f, 0.95f, 1.0f); + GLES20.glClearStencil(0); + } + + @Override + public boolean processedTile() { + return true; + } +} diff --git a/src/org/mapsforge/android/glrenderer/PolygonLayer.java b/src/org/mapsforge/android/glrenderer/PolygonLayer.java new file mode 100644 index 00000000..dea0294d --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/PolygonLayer.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ +package org.mapsforge.android.glrenderer; + +import java.util.LinkedList; + +class PolygonLayer extends Layer { + int fadeLevel; + private boolean first = true; + private float originX; + private float originY; + + PolygonLayer(int layer, int color, int fade) { + super(layer, color); + fadeLevel = fade; + curItem = LayerPool.get(); + pool = new LinkedList(); + pool.add(curItem); + } + + void addPolygon(float[] points, int pos, int length) { + + verticesCnt += length / 2 + 2; + + if (first) { + first = false; + originX = points[pos]; + originY = points[pos + 1]; + } + + float[] curVertices = curItem.vertices; + int outPos = curItem.used; + + if (outPos == PoolItem.SIZE) { + curVertices = getNextItem(); + outPos = 0; + } + + curVertices[outPos++] = originX; + curVertices[outPos++] = originY; + + int remaining = length; + int inPos = pos; + while (remaining > 0) { + + if (outPos == PoolItem.SIZE) { + curVertices = getNextItem(); + outPos = 0; + } + + int len = remaining; + if (len > (PoolItem.SIZE) - outPos) + len = (PoolItem.SIZE) - outPos; + + System.arraycopy(points, inPos, curVertices, outPos, len); + outPos += len; + inPos += len; + remaining -= len; + } + + if (outPos == PoolItem.SIZE) { + curVertices = getNextItem(); + outPos = 0; + } + + curVertices[outPos++] = points[pos + 0]; + curVertices[outPos++] = points[pos + 1]; + + curItem.used = outPos; + } +} diff --git a/src/org/mapsforge/android/glrenderer/PolygonLayers.java b/src/org/mapsforge/android/glrenderer/PolygonLayers.java new file mode 100644 index 00000000..056ad431 --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/PolygonLayers.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ +package org.mapsforge.android.glrenderer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import org.mapsforge.android.utils.FastMath; +import org.mapsforge.core.Tile; + +import android.util.SparseArray; + +class PolygonLayers { + private static final int NUM_VERTEX_FLOATS = 2; + private static final float[] mFillCoords = { -2, Tile.TILE_SIZE + 1, Tile.TILE_SIZE + 1, Tile.TILE_SIZE + 1, -2, + -2, Tile.TILE_SIZE + 1, -2 }; + + private static byte[] mByteFillCoords = null; + + private SparseArray layers; + + PolygonLayer[] array = null; + int size; + + PolygonLayers() { + layers = new SparseArray(10); + size = 4; + } + + PolygonLayer getLayer(int layer, int color, int fade) { + PolygonLayer l = layers.get(layer); + if (l != null) { + if (color == l.color) + return l; + + return getLayer(layer + 1, color, fade); + } + + l = new PolygonLayer(layer, color, fade); + layers.put(layer, l); + return l; + } + + FloatBuffer compileLayerData(FloatBuffer buf) { + FloatBuffer fbuf = buf; + + array = new PolygonLayer[layers.size()]; + + for (int i = 0, n = layers.size(); i < n; i++) { + PolygonLayer l = layers.valueAt(i); + array[i] = l; + size += l.verticesCnt; + } + + size *= NUM_VERTEX_FLOATS; + + if (buf == null || buf.capacity() < size) { + ByteBuffer bbuf = ByteBuffer.allocateDirect(size * 4).order(ByteOrder.nativeOrder()); + // Log.d("GLMap", "allocate buffer " + size); + fbuf = bbuf.asFloatBuffer(); + } else { + fbuf.position(0); + } + + fbuf.put(mFillCoords, 0, 8); + int pos = 4; + + for (int i = 0, n = array.length; i < n; i++) { + PolygonLayer l = array[i]; + + for (PoolItem item : l.pool) { + fbuf.put(item.vertices, 0, item.used); + } + + l.offset = pos; + pos += l.verticesCnt; + + LayerPool.add(l.pool); + l.pool = null; + } + + fbuf.position(0); + + // not needed for drawing + layers = null; + + return fbuf; + } + + ByteBuffer compileLayerData(ByteBuffer buf) { + ByteBuffer bbuf = buf; + + array = new PolygonLayer[layers.size()]; + + for (int i = 0, n = layers.size(); i < n; i++) { + PolygonLayer l = layers.valueAt(i); + array[i] = l; + size += l.verticesCnt; + } + + size *= NUM_VERTEX_FLOATS; + + if (buf == null || buf.capacity() < size * 2) { + bbuf = ByteBuffer.allocateDirect(size * 2).order(ByteOrder.nativeOrder()); + } else { + bbuf.position(0); + } + + byte[] data = new byte[PoolItem.SIZE * 2]; + + if (mByteFillCoords == null) { + mByteFillCoords = new byte[16]; + FastMath.convertFloatToHalf(mFillCoords[0], mByteFillCoords, 0); + FastMath.convertFloatToHalf(mFillCoords[1], mByteFillCoords, 2); + FastMath.convertFloatToHalf(mFillCoords[2], mByteFillCoords, 4); + FastMath.convertFloatToHalf(mFillCoords[3], mByteFillCoords, 6); + FastMath.convertFloatToHalf(mFillCoords[4], mByteFillCoords, 8); + FastMath.convertFloatToHalf(mFillCoords[5], mByteFillCoords, 10); + FastMath.convertFloatToHalf(mFillCoords[6], mByteFillCoords, 12); + FastMath.convertFloatToHalf(mFillCoords[7], mByteFillCoords, 14); + } + + bbuf.put(mByteFillCoords, 0, 16); + int pos = 4; + + for (int i = 0, n = array.length; i < n; i++) { + PolygonLayer l = array[i]; + + for (int k = 0, m = l.pool.size(); k < m; k++) { + PoolItem item = l.pool.get(k); + PoolItem.toHalfFloat(item, data); + bbuf.put(data, 0, item.used * 2); + } + + l.offset = pos; + pos += l.verticesCnt; + + LayerPool.add(l.pool); + l.pool = null; + } + + bbuf.position(0); + + // not needed for drawing + layers = null; + + return bbuf; + } +} diff --git a/src/org/mapsforge/android/glrenderer/PoolItem.java b/src/org/mapsforge/android/glrenderer/PoolItem.java new file mode 100644 index 00000000..c1edf076 --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/PoolItem.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ + +package org.mapsforge.android.glrenderer; + +// TODO use byte[] for half-float, not converting on compilation (in glThread) + +class PoolItem { + final float[] vertices; + // final byte[] vertices; + int used; + + PoolItem() { + vertices = new float[SIZE]; + // vertices = new byte[SIZE]; + used = 0; + } + + static int SIZE = 256; + + private static final byte b0x7c = (byte) 0x7c; + private static final byte b0x00 = (byte) 0x00; + private static final byte b0x01 = (byte) 0x01; + private static final byte b0xfc = (byte) 0xfc; + private static final byte b0x80 = (byte) 0x80; + private static final byte b0x7b = (byte) 0x7b; + private static final byte b0xff = (byte) 0xff; + private static final byte b0xfb = (byte) 0xfb; + private static final float FLOAT_HALF_PREC = 5.96046E-8f; + private static final float FLOAT_HALF_MAX = 65504f; + + static void toHalfFloat(PoolItem item, byte[] data) { + int out = 0; + for (int j = 0; j < item.used; j++) { + float flt = item.vertices[j]; + + if (flt == 0f) { + data[out++] = b0x00; + data[out++] = b0x00; + } else if (flt == -0f) { + data[out++] = b0x00; + data[out++] = b0x80; + } else if (flt > FLOAT_HALF_MAX) { + if (flt == Float.POSITIVE_INFINITY) { + data[out++] = b0x00; + data[out++] = b0x7c; + } else { + data[out++] = b0xff; + data[out++] = b0x7b; + } + } else if (flt < -FLOAT_HALF_MAX) { + if (flt == Float.NEGATIVE_INFINITY) { + data[out++] = b0x00; + data[out++] = b0xfc; + } else { + data[out++] = b0xff; + data[out++] = b0xfb; + } + } else if (flt > 0f && flt < FLOAT_HALF_PREC) { + data[out++] = b0x01; + data[out++] = b0x00; + } else if (flt < 0f && flt > -FLOAT_HALF_PREC) { + data[out++] = b0x01; + data[out++] = b0x80; + } else { + int f = Float.floatToIntBits(flt); + + if (f == 0x7fc00000) + throw new UnsupportedOperationException("NaN to half conversion not supported!"); + + data[out++] = (byte) ((f >> 13) & 0xff); + + data[out++] = (byte) (((f >> 24) & 0x80) | ((((f & 0x7f800000) - 0x38000000) >> 21) & 0x7c) | ((f >> 21) & 0x03)); + } + } + } +} diff --git a/src/org/mapsforge/android/glrenderer/Shaders.java b/src/org/mapsforge/android/glrenderer/Shaders.java new file mode 100644 index 00000000..b9bf7766 --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/Shaders.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ + +package org.mapsforge.android.glrenderer; + +class Shaders { + final static String gLineVertexShader = "" + "precision mediump float; \n" + "uniform mat4 u_center;" + + "uniform float u_width;" + "attribute vec4 a_position;" + "attribute vec2 a_st;" + "varying vec2 v_st;" + + "void main() {" + " gl_Position = u_center * a_position;" + " v_st = a_st;" + "}"; + + final static String gLineFragmentShader = "" + "#extension GL_OES_standard_derivatives : enable\n" + + "precision mediump float;" + "uniform float u_width;" + "uniform int u_mode;" + "uniform vec4 u_color;" + + "const float zero = 0.0;" + "const int standard = 0;" + "const int fixed_width = 2;" + + "varying vec2 v_st;" + "void main() {" + " gl_FragColor = u_color;" + " float fuzz = fwidth(v_st.s);" + + " float len = abs(v_st.s) - u_width;" + " if (u_mode != fixed_width) {" + + " if (v_st.t != zero){ " + " fuzz = max(fuzz, fwidth(v_st.t));" + + " len = length(v_st) - u_width;" + " } " + + // branching is not recommended... + // " if (- fuzz > len) " + + // " gl_FragColor = u_color;" + + // " if (len < -fuzz)" + + // " discard;" + + // " else " + + " if (len > -fuzz)" + " gl_FragColor *= smoothstep(fuzz , -fuzz , len);" + " } else { " + + // just guesswork.. looks ok for fixed line width >= 0.5 + " if (len > -fuzz)" + " gl_FragColor *= smoothstep(fuzz*0.5, -fuzz, len);" + " }" + "}"; + + // final static String gLineFragmentShader = "" + + // "#extension GL_OES_standard_derivatives : enable\n" + + // "precision mediump float;" + + // "uniform float u_width;" + + // "uniform int u_mode;" + + // "uniform vec4 u_color;" + + // "varying vec2 v_st;" + + // "void main() {" + + // " gl_FragColor = u_color;" + + // "}"; + + final static String gLineFragmentShaderSimple = "" + "precision mediump float;" + "uniform vec4 u_color;" + + "uniform float u_width;" + "varying vec2 v_st;" + "void main() {" + " vec4 color = u_color;" + + " float len;" + " if (v_st.t == 0.0) " + " len = abs(v_st.s);" + " else " + + " len = length(v_st);" + " if (len > 0.4) {" + + " color = u_color * (smoothstep(0.2, 1.0, (u_width + 0.3) - len));" + "}" + " gl_FragColor = color;" + + "}"; + + final static String gPolygonVertexShader = "" + "precision mediump float; \n" + "uniform mat4 u_center;\n" + + "attribute vec4 a_position;" + "void main() {" + " gl_Position = u_center * a_position;" + "}"; + + final static String gPolygonFragmentShader = "" + "precision mediump float;" + "uniform vec4 u_color;" + + "void main() {" + " gl_FragColor = u_color;" + "}"; +} diff --git a/src/org/mapsforge/android/glrenderer/VertexBufferObject.java b/src/org/mapsforge/android/glrenderer/VertexBufferObject.java new file mode 100644 index 00000000..40c5d6e2 --- /dev/null +++ b/src/org/mapsforge/android/glrenderer/VertexBufferObject.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ + +package org.mapsforge.android.glrenderer; + +class VertexBufferObject { + final int id; + int size; + + VertexBufferObject(int id) { + this.id = id; + size = 0; + } +} \ No newline at end of file diff --git a/src/org/mapsforge/android/inputhandling/MapMover.java b/src/org/mapsforge/android/inputhandling/MapMover.java new file mode 100644 index 00000000..35394140 --- /dev/null +++ b/src/org/mapsforge/android/inputhandling/MapMover.java @@ -0,0 +1,240 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.inputhandling; + +import org.mapsforge.android.MapView; +import org.mapsforge.android.utils.PausableThread; + +import android.os.SystemClock; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** + * A MapMover moves the map horizontally and vertically at a configurable speed. It runs in a separate thread to avoid + * blocking the UI thread. + */ +public class MapMover extends PausableThread implements KeyEvent.Callback { + + private static final int DEFAULT_MOVE_SPEED_FACTOR = 10; + private static final int FRAME_LENGTH_IN_MS = 15; + private static final float MOVE_SPEED = 0.2f; + private static final String THREAD_NAME = "MapMover"; + private static final float TRACKBALL_MOVE_SPEED_FACTOR = 40; + + private final MapView mMapView; + private float mMoveSpeedFactor; + private float mMoveX; + private float mMoveY; + private long mTimePrevious; + + /** + * @param mapView + * the MapView which should be moved by this MapMover. + */ + public MapMover(MapView mapView) { + super(); + mMapView = mapView; + mMoveSpeedFactor = DEFAULT_MOVE_SPEED_FACTOR; + } + + /** + * @return the move speed factor, used for trackball and keyboard events. + */ + public float getMoveSpeedFactor() { + return mMoveSpeedFactor; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { + if (!mMapView.isClickable()) { + return false; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + moveLeft(); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + moveRight(); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + moveUp(); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + moveDown(); + return true; + } + return false; + } + + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent keyEvent) { + return false; + } + + @Override + public boolean onKeyMultiple(int keyCode, int count, KeyEvent keyEvent) { + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { + if (!mMapView.isClickable()) { + return false; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mMoveX = 0; + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mMoveY = 0; + return true; + } + return false; + } + + /** + * @param motionEvent + * a trackball event which should be handled. + * @return true if the event was handled, false otherwise. + */ + public boolean onTrackballEvent(MotionEvent motionEvent) { + if (!mMapView.isClickable()) { + return false; + } + + if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) { + float mapMoveX = motionEvent.getX() * TRACKBALL_MOVE_SPEED_FACTOR * getMoveSpeedFactor(); + float mapMoveY = motionEvent.getY() * TRACKBALL_MOVE_SPEED_FACTOR * getMoveSpeedFactor(); + + // mapView.getFrameBuffer().matrixPostTranslate(mapMoveX, + // mapMoveY); + mMapView.getMapPosition().moveMap(mapMoveX, mapMoveY); + mMapView.redrawTiles(); + return true; + } + return false; + } + + /** + * Sets the move speed factor of the map, used for trackball and keyboard events. + * + * @param moveSpeedFactor + * the factor by which the move speed of the map will be multiplied. + * @throws IllegalArgumentException + * if the new move speed factor is negative. + */ + public void setMoveSpeedFactor(float moveSpeedFactor) { + if (moveSpeedFactor < 0) { + throw new IllegalArgumentException(); + } + mMoveSpeedFactor = moveSpeedFactor; + } + + /** + * Stops moving the map completely. + */ + public void stopMove() { + mMoveX = 0; + mMoveY = 0; + } + + private void moveDown() { + if (mMoveY > 0) { + // stop moving the map vertically + mMoveY = 0; + } else if (mMoveY == 0) { + // start moving the map + mMoveY = -MOVE_SPEED * mMoveSpeedFactor; + mTimePrevious = SystemClock.uptimeMillis(); + synchronized (this) { + notify(); + } + } + } + + private void moveLeft() { + if (mMoveX < 0) { + // stop moving the map horizontally + mMoveX = 0; + } else if (mMoveX == 0) { + // start moving the map + mMoveX = MOVE_SPEED * mMoveSpeedFactor; + mTimePrevious = SystemClock.uptimeMillis(); + synchronized (this) { + notify(); + } + } + } + + private void moveRight() { + if (mMoveX > 0) { + // stop moving the map horizontally + mMoveX = 0; + } else if (mMoveX == 0) { + // start moving the map + mMoveX = -MOVE_SPEED * mMoveSpeedFactor; + mTimePrevious = SystemClock.uptimeMillis(); + synchronized (this) { + notify(); + } + } + } + + private void moveUp() { + if (mMoveY < 0) { + // stop moving the map vertically + mMoveY = 0; + } else if (mMoveY == 0) { + // start moving the map + mMoveY = MOVE_SPEED * mMoveSpeedFactor; + mTimePrevious = SystemClock.uptimeMillis(); + synchronized (this) { + notify(); + } + } + } + + @Override + protected void afterPause() { + mTimePrevious = SystemClock.uptimeMillis(); + } + + @Override + protected void doWork() throws InterruptedException { + // calculate the time difference to previous call + long timeCurrent = SystemClock.uptimeMillis(); + long timeElapsed = timeCurrent - mTimePrevious; + mTimePrevious = timeCurrent; + + // add the movement to the transformation matrices + // mapView.getFrameBuffer().matrixPostTranslate(timeElapsed * + // moveX, timeElapsed * moveY); + + // move the map and the overlays + mMapView.getMapPosition().moveMap(timeElapsed * mMoveX, timeElapsed * mMoveY); + mMapView.redrawTiles(); + sleep(FRAME_LENGTH_IN_MS); + } + + @Override + protected String getThreadName() { + return THREAD_NAME; + } + + @Override + protected boolean hasWork() { + return mMoveX != 0 || mMoveY != 0; + } +} diff --git a/src/org/mapsforge/android/inputhandling/ScaleListener.java b/src/org/mapsforge/android/inputhandling/ScaleListener.java new file mode 100644 index 00000000..29520985 --- /dev/null +++ b/src/org/mapsforge/android/inputhandling/ScaleListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.inputhandling; + +import org.mapsforge.android.MapView; +import org.mapsforge.android.MapViewPosition; + +import android.view.ScaleGestureDetector; + +class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener { + private final MapView mMapView; + private float mFocusX; + private float mFocusY; + private MapViewPosition mMapPosition; + + /** + * Creates a new ScaleListener for the given MapView. + * + * @param mapView + * the MapView which should be scaled. + */ + ScaleListener(MapView mapView) { + mMapView = mapView; + } + + @Override + public boolean onScale(ScaleGestureDetector scaleGestureDetector) { + float scaleFactor = scaleGestureDetector.getScaleFactor(); + + mMapPosition.scaleMap(scaleFactor, mFocusX, mFocusY); + mMapView.redrawTiles(); + + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { + mFocusX = scaleGestureDetector.getFocusX(); + mFocusY = scaleGestureDetector.getFocusY(); + + mFocusX -= ((mMapView.getWidth() >> 1)); + mFocusY -= ((mMapView.getHeight() >> 1)); + mMapPosition = mMapView.getMapPosition(); + + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) { + // do nothing + } +} diff --git a/src/org/mapsforge/android/inputhandling/TouchHandler.java b/src/org/mapsforge/android/inputhandling/TouchHandler.java new file mode 100644 index 00000000..2260a74e --- /dev/null +++ b/src/org/mapsforge/android/inputhandling/TouchHandler.java @@ -0,0 +1,300 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.inputhandling; + +import org.mapsforge.android.MapView; +import org.mapsforge.core.Tile; + +import android.content.Context; +import android.os.CountDownTimer; +import android.util.Log; +import android.view.GestureDetector; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.ViewConfiguration; +import android.view.animation.DecelerateInterpolator; +import android.widget.Scroller; + +/** + * Implementation for multi-touch capable devices. + */ +public class TouchHandler { + private static final int INVALID_POINTER_ID = -1; + + /** + * is pritected correct? share MapView with inner class + */ + protected final MapView mMapView; + + private final float mMapMoveDelta; + private boolean mMoveThresholdReached; + private float mPreviousPositionX; + private float mPreviousPositionY; + private int mActivePointerId; + + private final ScaleGestureDetector mScaleGestureDetector; + private final GestureDetector mGestureDetector; + + /** + * @param context + * the Context + * @param mapView + * the MapView + */ + public TouchHandler(Context context, MapView mapView) { + ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + mMapView = mapView; + mMapMoveDelta = viewConfiguration.getScaledTouchSlop(); + mActivePointerId = INVALID_POINTER_ID; + mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener(mMapView)); + mGestureDetector = new GestureDetector(new MapGestureDetector(mMapView)); + } + + private static int getAction(MotionEvent motionEvent) { + return motionEvent.getAction() & MotionEvent.ACTION_MASK; + } + + private boolean onActionCancel() { + mActivePointerId = INVALID_POINTER_ID; + return true; + } + + private boolean onActionDown(MotionEvent motionEvent) { + mPreviousPositionX = motionEvent.getX(); + mPreviousPositionY = motionEvent.getY(); + mMoveThresholdReached = false; + // save the ID of the pointer + mActivePointerId = motionEvent.getPointerId(0); + return true; + } + + private boolean onActionMove(MotionEvent motionEvent) { + int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); + + // calculate the distance between previous and current position + float moveX = motionEvent.getX(pointerIndex) - mPreviousPositionX; + float moveY = motionEvent.getY(pointerIndex) - mPreviousPositionY; + boolean scaling = mScaleGestureDetector.isInProgress(); + if (!scaling && !mMoveThresholdReached) { + + if (Math.abs(moveX) > 3 * mMapMoveDelta || Math.abs(moveY) > 3 * mMapMoveDelta) { + // the map movement threshold has been reached + // longPressDetector.pressStop(); + mMoveThresholdReached = true; + + // save the position of the event + mPreviousPositionX = motionEvent.getX(pointerIndex); + mPreviousPositionY = motionEvent.getY(pointerIndex); + } + return true; + } + + // save the position of the event + mPreviousPositionX = motionEvent.getX(pointerIndex); + mPreviousPositionY = motionEvent.getY(pointerIndex); + + if (scaling) { + return true; + } + + mMapView.getMapPosition().moveMap(moveX, moveY); + mMapView.redrawTiles(); + + return true; + } + + // private boolean onActionPointerDown(MotionEvent motionEvent) { + // longPressDetector.pressStop(); + // multiTouchDownTime = motionEvent.getEventTime(); + // return true; + // } + + private boolean onActionPointerUp(MotionEvent motionEvent) { + + // extract the index of the pointer that left the touch sensor + int pointerIndex = (motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + if (motionEvent.getPointerId(pointerIndex) == mActivePointerId) { + // the active pointer has gone up, choose a new one + if (pointerIndex == 0) { + pointerIndex = 1; + } else { + pointerIndex = 0; + } + // save the position of the event + mPreviousPositionX = motionEvent.getX(pointerIndex); + mPreviousPositionY = motionEvent.getY(pointerIndex); + mActivePointerId = motionEvent.getPointerId(pointerIndex); + } + + // calculate the time difference since the pointer has gone down + // long multiTouchTime = motionEvent.getEventTime() - + // multiTouchDownTime; + // if (multiTouchTime < doubleTapTimeout) { + // // multi-touch tap event, zoom out + // previousEventTap = false; + // mapView.zoom((byte) -1); + // } + return true; + } + + /** + * @param motionEvent + * ... + * @return ... + */ + private boolean onActionUp(MotionEvent motionEvent) { + // longPressDetector.pressStop(); + // int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); + + mActivePointerId = INVALID_POINTER_ID; + // if (mMoveThresholdReached // || longPressDetector.isEventHandled() + // ) { + // mPreviousEventTap = false; + // } else { + // if (mPreviousEventTap) { + // + // } else { + // mPreviousEventTap = true; + // } + // + // // store the position and the time of this tap event + // mPreviousTapX = motionEvent.getX(pointerIndex); + // mPreviousTapY = motionEvent.getY(pointerIndex); + // mPreviousTapTime = motionEvent.getEventTime(); + // + // } + return true; + } + + /** + * @param motionEvent + * ... + * @return ... + */ + public boolean handleMotionEvent(MotionEvent motionEvent) { + // workaround for a bug in the ScaleGestureDetector, see Android issue + // #12976 + if (motionEvent.getAction() != MotionEvent.ACTION_MOVE || motionEvent.getPointerCount() > 1) { + mScaleGestureDetector.onTouchEvent(motionEvent); + } + + mGestureDetector.onTouchEvent(motionEvent); + // if () { + // // mActivePointerId = INVALID_POINTER_ID; + // // return true; + // } + int action = getAction(motionEvent); + + if (action == MotionEvent.ACTION_DOWN) { + return onActionDown(motionEvent); + } else if (action == MotionEvent.ACTION_MOVE) { + return onActionMove(motionEvent); + } else if (action == MotionEvent.ACTION_UP) { + return onActionUp(motionEvent); + } else if (action == MotionEvent.ACTION_CANCEL) { + return onActionCancel(); + // } else if (action == MotionEvent.ACTION_POINTER_DOWN) { + // return onActionPointerDown(motionEvent); + } else if (action == MotionEvent.ACTION_POINTER_UP) { + return onActionPointerUp(motionEvent); + } + + // the event was not handled + return false; + } + + class MapGestureDetector extends SimpleOnGestureListener { + private Scroller mScroller; + private float mPrevX, mPrevY; + private CountDownTimer mTimer = null; + + public MapGestureDetector(MapView mapView) { + mScroller = new Scroller(mapView.getContext(), new DecelerateInterpolator()); + } + + @Override + public boolean onDown(MotionEvent e) { + mScroller.forceFinished(true); + + if (mTimer != null) { + mTimer.cancel(); + mTimer = null; + } + return true; + } + + boolean scroll() { + if (mScroller.isFinished()) { + return false; + } + mScroller.computeScrollOffset(); + float moveX = mScroller.getCurrX() - mPrevX; + float moveY = mScroller.getCurrY() - mPrevY; + + if (moveX >= 1 || moveY >= 1 || moveX <= -1 || moveY <= -1) { + mMapView.getMapPosition().moveMap(moveX, moveY); + mMapView.redrawTiles(); + mPrevX = mScroller.getCurrX(); + mPrevY = mScroller.getCurrY(); + } + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + int w = Tile.TILE_SIZE * 20; + int h = Tile.TILE_SIZE * 20; + mPrevX = 0; + mPrevY = 0; + + if (mTimer != null) { + mTimer.cancel(); + mTimer = null; + } + + mScroller.fling(0, 0, Math.round(velocityX) / 2, Math.round(velocityY) / 2, -w, w, -h, h); + // animate for two seconds + mTimer = new CountDownTimer(2000, 20) { + @Override + public void onTick(long tick) { + if (!scroll()) + cancel(); + } + + @Override + public void onFinish() { + // do nothing + } + }.start(); + + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + // mMapView.zoom((byte) 1); + Log.d("mapsforge", "long press"); + // return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + mMapView.zoom((byte) 1); + Log.d("mapsforge", "double tap"); + return true; + } + } +} diff --git a/src/org/mapsforge/android/inputhandling/ZoomAnimator.java b/src/org/mapsforge/android/inputhandling/ZoomAnimator.java new file mode 100644 index 00000000..16833ac6 --- /dev/null +++ b/src/org/mapsforge/android/inputhandling/ZoomAnimator.java @@ -0,0 +1,121 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.inputhandling; + +import org.mapsforge.android.MapView; +import org.mapsforge.android.utils.PausableThread; + +import android.os.SystemClock; + +/** + * A ZoomAnimator handles the zoom-in and zoom-out animations of the corresponding MapView. It runs in a separate thread + * to avoid blocking the UI thread. + */ +public class ZoomAnimator extends PausableThread { + private static final int DEFAULT_DURATION = 250; + private static final int FRAME_LENGTH_IN_MS = 15; + private static final String THREAD_NAME = "ZoomAnimator"; + + private boolean mExecuteAnimation; + private final MapView mMapView; + // private float mPivotX; + // private float mPivotY; + private float mScaleFactorApplied; + private long mTimeStart; + private float mZoomDifference; + private float mZoomEnd; + private float mZoomStart; + + /** + * @param mapView + * the MapView whose zoom level changes should be animated. + */ + public ZoomAnimator(MapView mapView) { + super(); + mMapView = mapView; + } + + /** + * @return true if the ZoomAnimator is working, false otherwise. + */ + public boolean isExecuting() { + return mExecuteAnimation; + } + + /** + * Sets the parameters for the zoom animation. + * + * @param zoomStart + * the zoom factor at the begin of the animation. + * @param zoomEnd + * the zoom factor at the end of the animation. + * @param pivotX + * the x coordinate of the animation center. + * @param pivotY + * the y coordinate of the animation center. + */ + public void setParameters(float zoomStart, float zoomEnd, float pivotX, float pivotY) { + mZoomStart = zoomStart; + mZoomEnd = zoomEnd; + // mPivotX = pivotX; + // mPivotY = pivotY; + } + + /** + * Starts a zoom animation with the current parameters. + */ + public void startAnimation() { + mZoomDifference = mZoomEnd - mZoomStart; + mScaleFactorApplied = mZoomStart; + mExecuteAnimation = true; + mTimeStart = SystemClock.uptimeMillis(); + synchronized (this) { + notify(); + } + } + + @Override + protected void doWork() throws InterruptedException { + // calculate the elapsed time + long timeElapsed = SystemClock.uptimeMillis() - mTimeStart; + float timeElapsedPercent = Math.min(1, timeElapsed / (float) DEFAULT_DURATION); + + // calculate the zoom and scale values at the current moment + float currentZoom = mZoomStart + timeElapsedPercent * mZoomDifference; + float scaleFactor = currentZoom / mScaleFactorApplied; + mScaleFactorApplied *= scaleFactor; + // mapView.getFrameBuffer().matrixPostScale(scaleFactor, scaleFactor, + // pivotX, pivotY); + + // check if the animation time is over + if (timeElapsed >= DEFAULT_DURATION) { + mExecuteAnimation = false; + mMapView.redrawTiles(); + } else { + mMapView.postInvalidate(); + sleep(FRAME_LENGTH_IN_MS); + } + } + + @Override + protected String getThreadName() { + return THREAD_NAME; + } + + @Override + protected boolean hasWork() { + return mExecuteAnimation; + } +} diff --git a/src/org/mapsforge/android/mapgenerator/JobParameters.java b/src/org/mapsforge/android/mapgenerator/JobParameters.java new file mode 100644 index 00000000..e5fdcd8a --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/JobParameters.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import java.io.Serializable; + +/** + * A JobParameters instance is a simple DTO to store the rendering parameters for a job. + */ +public class JobParameters implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The render theme which should be used. + */ + public final JobTheme jobTheme; + + /** + * The text scale factor which should applied to the render theme. + */ + public final float textScale; + + private final int mHashCodeValue; + + /** + * @param jobTheme + * render theme which should be used. + * @param textScale + * the text scale factor which should applied to the render theme. + */ + public JobParameters(JobTheme jobTheme, float textScale) { + this.jobTheme = jobTheme; + this.textScale = textScale; + mHashCodeValue = calculateHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof JobParameters)) { + return false; + } + JobParameters other = (JobParameters) obj; + if (jobTheme == null) { + if (other.jobTheme != null) { + return false; + } + } else if (!jobTheme.equals(other.jobTheme)) { + return false; + } + if (Float.floatToIntBits(textScale) != Float.floatToIntBits(other.textScale)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return mHashCodeValue; + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + ((jobTheme == null) ? 0 : jobTheme.hashCode()); + result = 31 * result + Float.floatToIntBits(textScale); + return result; + } +} diff --git a/src/org/mapsforge/android/mapgenerator/JobQueue.java b/src/org/mapsforge/android/mapgenerator/JobQueue.java new file mode 100644 index 00000000..9b4db9d8 --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/JobQueue.java @@ -0,0 +1,121 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import java.util.ArrayList; +import java.util.PriorityQueue; + +import org.mapsforge.android.MapView; + +import android.os.SystemClock; + +/** + * A JobQueue keeps the list of pending jobs for a MapView and prioritizes them. + */ +public class JobQueue { + private static final int INITIAL_CAPACITY = 128; + + private final MapView mMapView; + private PriorityQueue mPriorityQueue; + private boolean mScheduleNeeded; + + /** + * @param mapView + * the MapView whose jobs should be organized. + */ + public JobQueue(MapView mapView) { + mMapView = mapView; + mPriorityQueue = new PriorityQueue(INITIAL_CAPACITY); + } + + /** + * Adds the given job to this queue. Does nothing if the given job is already in this queue. + * + * @param mapGeneratorJob + * the job to be added to this queue. + */ + public synchronized void addJob(MapGeneratorJob mapGeneratorJob) { + if (!mPriorityQueue.contains(mapGeneratorJob)) + // priorityQueue.remove(mapGeneratorJob); + { + mapGeneratorJob.tile.isLoading = true; + mPriorityQueue.offer(mapGeneratorJob); + } + } + + /** + * @param jobs + * the job to be added to this queue. + */ + public synchronized void setJobs(ArrayList jobs) { + mPriorityQueue.clear(); + for (MapGeneratorJob job : jobs) + mPriorityQueue.offer(job); + // priorityQueue.addAll(jobs); + mScheduleNeeded = true; + } + + /** + * Removes all jobs from this queue. + */ + public synchronized void clear() { + mPriorityQueue.clear(); + } + + /** + * @return true if this queue contains no jobs, false otherwise. + */ + public synchronized boolean isEmpty() { + return mPriorityQueue.isEmpty(); + } + + /** + * @return the most important job from this queue or null, if empty. + */ + public synchronized MapGeneratorJob poll() { + if (mScheduleNeeded) { + mScheduleNeeded = false; + schedule(); + } + return mPriorityQueue.poll(); + } + + /** + * Request a scheduling of all jobs that are currently in this queue. + */ + public synchronized void requestSchedule() { + mScheduleNeeded = true; + } + + /** + * Schedules all jobs in this queue. + */ + private void schedule() { + PriorityQueue tempJobQueue = new PriorityQueue(INITIAL_CAPACITY); + + TileScheduler.time = SystemClock.uptimeMillis(); + TileScheduler.mapPosition = mMapView.getMapPosition().getMapPosition(); + + while (!mPriorityQueue.isEmpty()) { + MapGeneratorJob mapGeneratorJob = mPriorityQueue.poll(); + double priority = TileScheduler.getPriority(mapGeneratorJob, mMapView); + mapGeneratorJob.setPriority(priority); + tempJobQueue.offer(mapGeneratorJob); + } + + mPriorityQueue = tempJobQueue; + } + +} diff --git a/src/org/mapsforge/android/mapgenerator/JobTheme.java b/src/org/mapsforge/android/mapgenerator/JobTheme.java new file mode 100644 index 00000000..b1913c54 --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/JobTheme.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.Serializable; + +/** + * A JobTheme defines the render theme which is used for a {@link MapGeneratorJob}. + */ +public interface JobTheme extends Serializable { + /** + * @return an InputStream to read the render theme data from. + * @throws FileNotFoundException + * if the render theme file cannot be found. + */ + InputStream getRenderThemeAsStream() throws FileNotFoundException; +} diff --git a/src/org/mapsforge/android/mapgenerator/MapDatabaseFactory.java b/src/org/mapsforge/android/mapgenerator/MapDatabaseFactory.java new file mode 100644 index 00000000..9ea103cb --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/MapDatabaseFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import org.mapsforge.mapdatabase.IMapDatabase; + +import android.util.AttributeSet; + +/** + * + * + */ +public final class MapDatabaseFactory { + private static final String MAP_DATABASE_ATTRIBUTE_NAME = "mapDatabase"; + + /** + * @param attributeSet + * A collection of attributes which includes the desired MapGenerator. + * @return a new MapGenerator instance. + */ + public static IMapDatabase createMapDatabase(AttributeSet attributeSet) { + String mapDatabaseName = attributeSet.getAttributeValue(null, MAP_DATABASE_ATTRIBUTE_NAME); + if (mapDatabaseName == null) { + return new org.mapsforge.mapdatabase.mapfile.MapDatabase(); + } + + MapDatabaseInternal mapDatabaseInternal = MapDatabaseInternal.valueOf(mapDatabaseName); + return MapDatabaseFactory.createMapDatabase(mapDatabaseInternal); + } + + /** + * @param mapDatabaseInternal + * the internal MapDatabase implementation. + * @return a new MapGenerator instance. + */ + public static IMapDatabase createMapDatabase(MapDatabaseInternal mapDatabaseInternal) { + switch (mapDatabaseInternal) { + case MAP_READER: + return new org.mapsforge.mapdatabase.mapfile.MapDatabase(); + case JSON_READER: + return new org.mapsforge.mapdatabase.json.MapDatabase(); + case POSTGIS_READER: + return new org.mapsforge.mapdatabase.postgis.MapDatabase(); + + } + + throw new IllegalArgumentException("unknown enum value: " + mapDatabaseInternal); + } + + private MapDatabaseFactory() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/android/mapgenerator/MapDatabaseInternal.java b/src/org/mapsforge/android/mapgenerator/MapDatabaseInternal.java new file mode 100644 index 00000000..da134d68 --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/MapDatabaseInternal.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +/** + * MapDatabase Implementations + */ +public enum MapDatabaseInternal { + /** + * ... + */ + MAP_READER, + + /** + * ... + */ + JSON_READER, + + /** + * ... + */ + POSTGIS_READER, + +} diff --git a/src/org/mapsforge/android/mapgenerator/MapGenerator.java b/src/org/mapsforge/android/mapgenerator/MapGenerator.java new file mode 100644 index 00000000..50386f16 --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/MapGenerator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import org.mapsforge.android.MapRenderer; +import org.mapsforge.android.MapView; +import org.mapsforge.core.GeoPoint; +import org.mapsforge.mapdatabase.IMapDatabase; + +/** + * A MapGenerator provides map tiles either by downloading or rendering them. + */ +public interface MapGenerator { + /** + * Called once at the end of the MapGenerator lifecycle. + */ + void cleanup(); + + /** + * Called when a job needs to be executed. + * + * @param mapGeneratorJob + * the job that should be executed. + * @return true if the job was executed successfully, false otherwise. + */ + boolean executeJob(MapGeneratorJob mapGeneratorJob); + + /** + * @return the start point of this MapGenerator (may be null). + */ + GeoPoint getStartPoint(); + + /** + * @return the start zoom level of this MapGenerator (may be null). + */ + Byte getStartZoomLevel(); + + /** + * @return the maximum zoom level that this MapGenerator supports. + */ + byte getZoomLevelMax(); + + /** + * @return true if this MapGenerator requires an Internet connection, false otherwise. + */ + boolean requiresInternetConnection(); + + /** + * @param mapView + * the MapView + * @return GLSurfaceView Renderer + */ + MapRenderer getMapRenderer(MapView mapView); + + /** + * @param mapDatabase + * the MapDatabase from which the map data will be read. + */ + void setMapDatabase(IMapDatabase mapDatabase); +} diff --git a/src/org/mapsforge/android/mapgenerator/MapGeneratorFactory.java b/src/org/mapsforge/android/mapgenerator/MapGeneratorFactory.java new file mode 100644 index 00000000..b34d068c --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/MapGeneratorFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import android.util.AttributeSet; + +/** + * A factory for the internal MapGenerator implementations. + */ +public final class MapGeneratorFactory { + private static final String MAP_GENERATOR_ATTRIBUTE_NAME = "mapGenerator"; + + /** + * @param attributeSet + * A collection of attributes which includes the desired MapGenerator. + * @return a new MapGenerator instance. + */ + public static MapGenerator createMapGenerator(AttributeSet attributeSet) { + String mapGeneratorName = attributeSet.getAttributeValue(null, MAP_GENERATOR_ATTRIBUTE_NAME); + if (mapGeneratorName == null) { + return new org.mapsforge.android.glrenderer.DatabaseRenderer(); + } + + MapGeneratorInternal mapGeneratorInternal = MapGeneratorInternal.valueOf(mapGeneratorName); + return MapGeneratorFactory.createMapGenerator(mapGeneratorInternal); + } + + /** + * @param mapGeneratorInternal + * the internal MapGenerator implementation. + * @return a new MapGenerator instance. + */ + public static MapGenerator createMapGenerator(MapGeneratorInternal mapGeneratorInternal) { + switch (mapGeneratorInternal) { + case SW_RENDERER: + return new org.mapsforge.android.swrenderer.DatabaseRenderer(); + case GL_RENDERER: + return new org.mapsforge.android.glrenderer.DatabaseRenderer(); + } + + throw new IllegalArgumentException("unknown enum value: " + mapGeneratorInternal); + } + + private MapGeneratorFactory() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/android/mapgenerator/MapGeneratorInternal.java b/src/org/mapsforge/android/mapgenerator/MapGeneratorInternal.java new file mode 100644 index 00000000..cab28c0e --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/MapGeneratorInternal.java @@ -0,0 +1,30 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +/** + * Enumeration of all internal MapGenerator implementations. + */ +public enum MapGeneratorInternal { + /** + * texture renderer. + */ + SW_RENDERER, + + /** + * opengl renderer. + */ + GL_RENDERER +} diff --git a/src/org/mapsforge/android/mapgenerator/MapGeneratorJob.java b/src/org/mapsforge/android/mapgenerator/MapGeneratorJob.java new file mode 100644 index 00000000..2e45b07b --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/MapGeneratorJob.java @@ -0,0 +1,188 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +import org.mapsforge.android.DebugSettings; + +import android.graphics.Bitmap; + +/** + * A MapGeneratorJob holds all immutable rendering parameters for a single map image together with a mutable priority + * field, which indicates the importance of this job. + */ +public class MapGeneratorJob implements Comparable, Serializable { + private static final long serialVersionUID = 1L; + + /** + * The debug settings for this job. + */ + public final DebugSettings debugSettings; + + /** + * The rendering parameters for this job. + */ + public final JobParameters jobParameters; + + /** + * The tile which should be generated. + */ + public final MapTile tile; + + private transient int mHashCodeValue; + private final MapGenerator mMapGenerator; + private transient double mPriority; + + /** + * bitmap passed to renderer + */ + private Bitmap mBitmap; + + /** + * @return ... + */ + public Bitmap getBitmap() { + return mBitmap; + } + + /** + * @param bitmap + * .. + */ + public void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + } + + private float mScale; + + /** + * @return scale the tile is rendered with + */ + public float getScale() { + return mScale; + } + + /** + * @param _scale + * for the tile to be rendered + */ + public void setScale(float _scale) { + mScale = _scale; + } + + /** + * Creates a new job for a MapGenerator with the given parameters. + * + * @param _tile + * the tile which should be generated. + * @param mapGenerator + * the MapGenerator for this job. + * @param _jobParameters + * the rendering parameters for this job. + * @param _debugSettings + * the debug settings for this job. + */ + public MapGeneratorJob(MapTile _tile, MapGenerator mapGenerator, + JobParameters _jobParameters, DebugSettings _debugSettings) { + tile = _tile; + mMapGenerator = mapGenerator; + jobParameters = _jobParameters; + debugSettings = _debugSettings; + calculateTransientValues(); + } + + @Override + public int compareTo(MapGeneratorJob o) { + if (mPriority < o.mPriority) { + return -1; + } else if (mPriority > o.mPriority) { + return 1; + } + return 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof MapGeneratorJob)) { + return false; + } + MapGeneratorJob other = (MapGeneratorJob) obj; + + if (debugSettings == null) { + if (other.debugSettings != null) { + return false; + } + } else if (!debugSettings.equals(other.debugSettings)) { + return false; + } + if (jobParameters == null) { + if (other.jobParameters != null) { + return false; + } + } else if (!jobParameters.equals(other.jobParameters)) { + return false; + } + if (mMapGenerator != other.mMapGenerator) { + return false; + } + if (tile == null) { + if (other.tile != null) { + return false; + } + } else if (!tile.equals(other.tile)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return mHashCodeValue; + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 1; + result = 31 * result + ((debugSettings == null) ? 0 : debugSettings.hashCode()); + result = 31 * result + ((jobParameters == null) ? 0 : jobParameters.hashCode()); + result = 31 * result + ((mMapGenerator == null) ? 0 : mMapGenerator.hashCode()); + result = 31 * result + ((tile == null) ? 0 : tile.hashCode()); + return result; + } + + /** + * Calculates the values of some transient variables. + */ + private void calculateTransientValues() { + mHashCodeValue = calculateHashCode(); + } + + private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { + objectInputStream.defaultReadObject(); + calculateTransientValues(); + } + + void setPriority(double priority) { + mPriority = priority; + } +} diff --git a/src/org/mapsforge/android/mapgenerator/MapTile.java b/src/org/mapsforge/android/mapgenerator/MapTile.java new file mode 100644 index 00000000..ebf608b3 --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/MapTile.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import org.mapsforge.core.Tile; + +/** + * + */ +public class MapTile extends Tile { + /** + * tile is loaded and ready for drawing. (set and used by render thread). + */ + public boolean isDrawn; + + /** + * tile is removed from JobQueue and loading in DatabaseRenderer. set by MapWorker. + */ + public boolean isLoading; + + /** + * tile is in view region. (set and used by render thread) + */ + public boolean isVisible; + + /** + * tile is used by render thread. set by updateVisibleList (main thread). + */ + public boolean isActive; + + /** + * distance from center, used in TileScheduler set by updateVisibleList. + */ + public long distance; + + /** + * @param tileX + * ... + * @param tileY + * ... + * @param zoomLevel + * .. + */ + public MapTile(long tileX, long tileY, byte zoomLevel) { + super(tileX, tileY, zoomLevel); + } +} diff --git a/src/org/mapsforge/android/mapgenerator/MapWorker.java b/src/org/mapsforge/android/mapgenerator/MapWorker.java new file mode 100644 index 00000000..0bc4b31c --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/MapWorker.java @@ -0,0 +1,90 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import org.mapsforge.android.MapRenderer; +import org.mapsforge.android.MapView; +import org.mapsforge.android.utils.PausableThread; + +/** + * A MapWorker uses a {@link MapGenerator} to generate map tiles. It runs in a separate thread to avoid blocking the UI + * thread. + */ +public class MapWorker extends PausableThread { + private static final String THREAD_NAME = "MapWorker"; + + private final JobQueue mJobQueue; + private MapGenerator mMapGenerator; + private MapRenderer mMapRenderer; + + /** + * @param mapView + * the MapView for which this MapWorker generates map tiles. + */ + public MapWorker(MapView mapView) { + super(); + mJobQueue = mapView.getJobQueue(); + } + + /** + * @param mapGenerator + * the MapGenerator which this MapWorker should use. + */ + public void setMapGenerator(MapGenerator mapGenerator) { + mMapGenerator = mapGenerator; + } + + /** + * @param mapRenderer + * the MapRenderer + */ + public void setMapRenderer(MapRenderer mapRenderer) { + mMapRenderer = mapRenderer; + } + + @Override + protected void afterRun() { + // empty + } + + @Override + protected void doWork() { + MapGeneratorJob mapGeneratorJob = mJobQueue.poll(); + + if (mMapGenerator == null || mapGeneratorJob == null) + return; + + boolean success = mMapGenerator.executeJob(mapGeneratorJob); + + if (!isInterrupted() && success) { + mMapRenderer.passTile(mapGeneratorJob); + } + } + + @Override + protected String getThreadName() { + return THREAD_NAME; + } + + @Override + protected int getThreadPriority() { + return (Thread.NORM_PRIORITY + Thread.MIN_PRIORITY) / 2; + } + + @Override + protected boolean hasWork() { + return !mJobQueue.isEmpty() && mMapRenderer.processedTile(); + } +} diff --git a/src/org/mapsforge/android/mapgenerator/TileCacheKey.java b/src/org/mapsforge/android/mapgenerator/TileCacheKey.java new file mode 100644 index 00000000..041ee89b --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/TileCacheKey.java @@ -0,0 +1,84 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +/** + * @author jeff + */ +public class TileCacheKey { + long x, y; + byte z; + int hash; + + /** + * + */ + public TileCacheKey() { + } + + /** + * @param key + * create new TileCacheKey for key + */ + public TileCacheKey(TileCacheKey key) { + this.x = key.x; + this.y = key.y; + this.z = key.z; + this.hash = key.hash; + } + + /** + * @param x + * Position + * @param y + * Position + * @param z + * Position + */ + public TileCacheKey(long x, long y, byte z) { + this.x = x; + this.y = y; + this.z = z; + hash = 7 * z + 31 * ((int) (x ^ (x >>> 32)) + 31 * (int) (y ^ (y >>> 32))); + } + + /** + * @param x + * Position + * @param y + * Position + * @param z + * Position + * @return self + */ + public TileCacheKey set(long x, long y, byte z) { + this.x = x; + this.y = y; + this.z = z; + hash = 7 * z + 31 * ((int) (x ^ (x >>> 32)) + 31 * (int) (y ^ (y >>> 32))); + return this; + } + + @Override + public boolean equals(Object obj) { + TileCacheKey other = (TileCacheKey) obj; + return (x == other.x && y == other.y && z == other.z); + } + + @Override + public int hashCode() { + return hash; + } +} diff --git a/src/org/mapsforge/android/mapgenerator/TileDistanceSort.java b/src/org/mapsforge/android/mapgenerator/TileDistanceSort.java new file mode 100644 index 00000000..6c866892 --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/TileDistanceSort.java @@ -0,0 +1,32 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import java.util.Comparator; + +/** + * + * + */ +public class TileDistanceSort implements Comparator { + + @Override + public int compare(MapTile tile1, MapTile tile2) { + if (tile1.distance == tile2.distance) + return 0; + + return tile1.distance > tile2.distance ? 1 : -1; + } +} diff --git a/src/org/mapsforge/android/mapgenerator/TileScheduler.java b/src/org/mapsforge/android/mapgenerator/TileScheduler.java new file mode 100644 index 00000000..32303d0e --- /dev/null +++ b/src/org/mapsforge/android/mapgenerator/TileScheduler.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.mapgenerator; + +import org.mapsforge.android.MapView; +import org.mapsforge.core.MapPosition; + +final class TileScheduler { + + static long time; + static MapPosition mapPosition; + + /** + * Calculates the priority for the given tile based on the current position and zoom level of the supplied MapView. + * The smaller the distance from the tile center to the MapView center, the higher its priority. If the zoom level + * of a tile differs from the zoom level of the MapView, its priority decreases. + * + * @param mapGeneratorJob + * the tile whose priority should be calculated. + * @param mapView + * the MapView whose current position and zoom level define the priority of the tile. + * @return the current priority of the tile. A smaller number means a higher priority. + */ + static double getPriority(MapGeneratorJob mapGeneratorJob, MapView mapView) { + MapTile tile = mapGeneratorJob.tile; + + // if (tile.isDrawn) { + // long diff = time - tile.loadTime; + // + // // check.. just in case + // if (diff > 0.0) { + // return (10000.0f / diff) * (tile.isVisible ? 1 : 5); // * tile.distance; + // } + // } + + // // calculate the center coordinates of the tile + // double tileCenterLongitude = MercatorProjection.pixelXToLongitude(tile.getCenterX(), tileZoomLevel); + // double tileCenterLatitude = MercatorProjection.pixelYToLatitude(tile.getCenterY(), tileZoomLevel); + // + // // calculate the Euclidian distance from the MapView center to the tile + // // center + // GeoPoint geoPoint = mapPosition.geoPoint; + // double longitudeDiff = geoPoint.getLongitude() - tileCenterLongitude; + // double latitudeDiff = geoPoint.getLatitude() - tileCenterLatitude; + // + // return Math.sqrt(longitudeDiff * longitudeDiff + latitudeDiff * latitudeDiff) + // * (tile.visible ? 1.0 : 1000.0); + // } + + return tile.distance / 1000.0; + } + + private TileScheduler() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/android/package-info.java b/src/org/mapsforge/android/package-info.java new file mode 100644 index 00000000..747969d4 --- /dev/null +++ b/src/org/mapsforge/android/package-info.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +/** + * The mapsforge-map library allows applications to render and display a map without Internet connection. It can + * be used on all Android devices running version 1.5 or higher. An application needs to extend the + * {@link org.mapsforge.android.MapActivity} class in order to use a + * {@link org.mapsforge.android.MapView}. More than one MapView instance may be used simultaneously. + *

+ * The most important classes and methods from the Google APIs Add-On are implemented. However, no API key is required and no abstract methods + * must be overridden. + *

+ * This software is a part of the mapsforge project and + * distributed under the LGPL3 license. All + * map data (c) OpenStreetMap contributors, CC-BY-SA license. + */ +package org.mapsforge.android; + diff --git a/src/org/mapsforge/android/rendertheme/AnyMatcher.java b/src/org/mapsforge/android/rendertheme/AnyMatcher.java new file mode 100644 index 00000000..e6d01086 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/AnyMatcher.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import org.mapsforge.core.Tag; + +final class AnyMatcher implements ElementMatcher, AttributeMatcher, ClosedMatcher { + private static final AnyMatcher INSTANCE = new AnyMatcher(); + + static AnyMatcher getInstance() { + return INSTANCE; + } + + /** + * Private constructor to prevent instantiation from other classes. + */ + private AnyMatcher() { + // do nothing + } + + @Override + public boolean isCoveredBy(AttributeMatcher attributeMatcher) { + return attributeMatcher == this; + } + + @Override + public boolean isCoveredBy(ClosedMatcher closedMatcher) { + return closedMatcher == this; + } + + @Override + public boolean isCoveredBy(ElementMatcher elementMatcher) { + return elementMatcher == this; + } + + @Override + public boolean matches(Closed closed) { + return true; + } + + @Override + public boolean matches(Element element) { + return true; + } + + @Override + public boolean matches(Tag[] tags) { + return true; + } +} diff --git a/src/org/mapsforge/android/rendertheme/AttributeMatcher.java b/src/org/mapsforge/android/rendertheme/AttributeMatcher.java new file mode 100644 index 00000000..4498ae57 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/AttributeMatcher.java @@ -0,0 +1,23 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import org.mapsforge.core.Tag; + +interface AttributeMatcher { + boolean isCoveredBy(AttributeMatcher attributeMatcher); + + boolean matches(Tag[] tags); +} diff --git a/src/org/mapsforge/android/rendertheme/Closed.java b/src/org/mapsforge/android/rendertheme/Closed.java new file mode 100644 index 00000000..9ca97b09 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/Closed.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +enum Closed { + ANY, NO, YES; +} diff --git a/src/org/mapsforge/android/rendertheme/ClosedMatcher.java b/src/org/mapsforge/android/rendertheme/ClosedMatcher.java new file mode 100644 index 00000000..d5ea30c9 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/ClosedMatcher.java @@ -0,0 +1,21 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +interface ClosedMatcher { + boolean isCoveredBy(ClosedMatcher closedMatcher); + + boolean matches(Closed closed); +} diff --git a/src/org/mapsforge/android/rendertheme/ClosedWayMatcher.java b/src/org/mapsforge/android/rendertheme/ClosedWayMatcher.java new file mode 100644 index 00000000..f5106cc0 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/ClosedWayMatcher.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +final class ClosedWayMatcher implements ClosedMatcher { + private static final ClosedWayMatcher INSTANCE = new ClosedWayMatcher(); + + static ClosedWayMatcher getInstance() { + return INSTANCE; + } + + /** + * Private constructor to prevent instantiation from other classes. + */ + private ClosedWayMatcher() { + // do nothing + } + + @Override + public boolean isCoveredBy(ClosedMatcher closedMatcher) { + return closedMatcher.matches(Closed.YES); + } + + @Override + public boolean matches(Closed closed) { + return closed == Closed.YES; + } +} diff --git a/src/org/mapsforge/android/rendertheme/Element.java b/src/org/mapsforge/android/rendertheme/Element.java new file mode 100644 index 00000000..5bdcdc98 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/Element.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +enum Element { + ANY, NODE, WAY; +} diff --git a/src/org/mapsforge/android/rendertheme/ElementMatcher.java b/src/org/mapsforge/android/rendertheme/ElementMatcher.java new file mode 100644 index 00000000..c605d897 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/ElementMatcher.java @@ -0,0 +1,21 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +interface ElementMatcher { + boolean isCoveredBy(ElementMatcher elementMatcher); + + boolean matches(Element element); +} diff --git a/src/org/mapsforge/android/rendertheme/ElementNodeMatcher.java b/src/org/mapsforge/android/rendertheme/ElementNodeMatcher.java new file mode 100644 index 00000000..c67f80b6 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/ElementNodeMatcher.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +final class ElementNodeMatcher implements ElementMatcher { + private static final ElementNodeMatcher INSTANCE = new ElementNodeMatcher(); + + static ElementNodeMatcher getInstance() { + return INSTANCE; + } + + /** + * Private constructor to prevent instantiation from other classes. + */ + private ElementNodeMatcher() { + // do nothing + } + + @Override + public boolean isCoveredBy(ElementMatcher elementMatcher) { + return elementMatcher.matches(Element.NODE); + } + + @Override + public boolean matches(Element element) { + return element == Element.NODE; + } +} diff --git a/src/org/mapsforge/android/rendertheme/ElementWayMatcher.java b/src/org/mapsforge/android/rendertheme/ElementWayMatcher.java new file mode 100644 index 00000000..2017d97c --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/ElementWayMatcher.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +final class ElementWayMatcher implements ElementMatcher { + private static final ElementWayMatcher INSTANCE = new ElementWayMatcher(); + + static ElementWayMatcher getInstance() { + return INSTANCE; + } + + /** + * Private constructor to prevent instantiation from other classes. + */ + private ElementWayMatcher() { + // do nothing + } + + @Override + public boolean isCoveredBy(ElementMatcher elementMatcher) { + return elementMatcher.matches(Element.WAY); + } + + @Override + public boolean matches(Element element) { + return element == Element.WAY; + } +} diff --git a/src/org/mapsforge/android/rendertheme/ExternalRenderTheme.java b/src/org/mapsforge/android/rendertheme/ExternalRenderTheme.java new file mode 100644 index 00000000..0b2aab15 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/ExternalRenderTheme.java @@ -0,0 +1,109 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; + +import org.mapsforge.android.mapgenerator.JobTheme; + +/** + * An ExternalRenderTheme allows for customizing the rendering style of the map via an XML file. + */ +public class ExternalRenderTheme implements JobTheme { + private static final long serialVersionUID = 1L; + + private final long mFileModificationDate; + private transient int mHashCodeValue; + private final String mRenderThemePath; + + /** + * @param renderThemePath + * the path to the XML render theme file. + * @throws FileNotFoundException + * if the file does not exist or cannot be read. + */ + public ExternalRenderTheme(String renderThemePath) throws FileNotFoundException { + File renderThemeFile = new File(renderThemePath); + if (!renderThemeFile.exists()) { + throw new FileNotFoundException("file does not exist: " + renderThemePath); + } else if (!renderThemeFile.isFile()) { + throw new FileNotFoundException("not a file: " + renderThemePath); + } else if (!renderThemeFile.canRead()) { + throw new FileNotFoundException("cannot read file: " + renderThemePath); + } + + mFileModificationDate = renderThemeFile.lastModified(); + if (mFileModificationDate == 0L) { + throw new FileNotFoundException("cannot read last modification time"); + } + mRenderThemePath = renderThemePath; + calculateTransientValues(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof ExternalRenderTheme)) { + return false; + } + ExternalRenderTheme other = (ExternalRenderTheme) obj; + if (mFileModificationDate != other.mFileModificationDate) { + return false; + } else if (mRenderThemePath == null && other.mRenderThemePath != null) { + return false; + } else if (mRenderThemePath != null && !mRenderThemePath.equals(other.mRenderThemePath)) { + return false; + } + return true; + } + + @Override + public InputStream getRenderThemeAsStream() throws FileNotFoundException { + return new FileInputStream(mRenderThemePath); + } + + @Override + public int hashCode() { + return mHashCodeValue; + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 1; + result = 31 * result + (int) (mFileModificationDate ^ (mFileModificationDate >>> 32)); + result = 31 * result + ((mRenderThemePath == null) ? 0 : mRenderThemePath.hashCode()); + return result; + } + + /** + * Calculates the values of some transient variables. + */ + private void calculateTransientValues() { + mHashCodeValue = calculateHashCode(); + } + + private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { + objectInputStream.defaultReadObject(); + calculateTransientValues(); + } +} diff --git a/src/org/mapsforge/android/rendertheme/InternalRenderTheme.java b/src/org/mapsforge/android/rendertheme/InternalRenderTheme.java new file mode 100644 index 00000000..23b523d5 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/InternalRenderTheme.java @@ -0,0 +1,42 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.io.InputStream; + +import org.mapsforge.android.mapgenerator.JobTheme; + +/** + * Enumeration of all internal rendering themes. + */ +public enum InternalRenderTheme implements JobTheme { + /** + * A rendering theme similar to the OpenStreetMap Osmarender style. + * + * @see Osmarender + */ + OSMARENDER("/org/mapsforge/android/rendertheme/osmarender/osmarender.xml"); + + private final String mPath; + + private InternalRenderTheme(String path) { + mPath = path; + } + + @Override + public InputStream getRenderThemeAsStream() { + return Thread.currentThread().getClass().getResourceAsStream(mPath); + } +} diff --git a/src/org/mapsforge/android/rendertheme/LinearWayMatcher.java b/src/org/mapsforge/android/rendertheme/LinearWayMatcher.java new file mode 100644 index 00000000..51f4316b --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/LinearWayMatcher.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +final class LinearWayMatcher implements ClosedMatcher { + private static final LinearWayMatcher INSTANCE = new LinearWayMatcher(); + + static LinearWayMatcher getInstance() { + return INSTANCE; + } + + /** + * Private constructor to prevent instantiation from other classes. + */ + private LinearWayMatcher() { + // do nothing + } + + @Override + public boolean isCoveredBy(ClosedMatcher closedMatcher) { + return closedMatcher.matches(Closed.NO); + } + + @Override + public boolean matches(Closed closed) { + return closed == Closed.NO; + } +} diff --git a/src/org/mapsforge/android/rendertheme/MatchingCacheKey.java b/src/org/mapsforge/android/rendertheme/MatchingCacheKey.java new file mode 100644 index 00000000..c9a33e02 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/MatchingCacheKey.java @@ -0,0 +1,79 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import org.mapsforge.core.Tag; + +class MatchingCacheKey { + private final int mHashCodeValue; + final Tag[] mTags; + final byte mZoomLevel; + + MatchingCacheKey(Tag[] tags, byte zoomLevel) { + mTags = tags; + mZoomLevel = zoomLevel; + mHashCodeValue = calculateHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof MatchingCacheKey)) { + return false; + } + MatchingCacheKey other = (MatchingCacheKey) obj; + + if (mZoomLevel != other.mZoomLevel) + return false; + + if (mTags == null) { + return (other.mTags == null); + } else if (other.mTags == null) + return false; + + int length = mTags.length; + if (length != other.mTags.length) { + return false; + } + + for (int i = 0; i < length; i++) + if (mTags[i] != other.mTags[i]) + return false; + + return true; + } + + @Override + public int hashCode() { + return mHashCodeValue; + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + + for (int i = 0, n = mTags.length; i < n; i++) { + if (mTags[i] == null) // FIXME + break; + result = 31 * result + mTags[i].hashCode(); + } + result = 31 * result + mZoomLevel; + + return result; + } +} diff --git a/src/org/mapsforge/android/rendertheme/MultiKeyMatcher.java b/src/org/mapsforge/android/rendertheme/MultiKeyMatcher.java new file mode 100644 index 00000000..96805a01 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/MultiKeyMatcher.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.util.List; + +import org.mapsforge.core.Tag; + +class MultiKeyMatcher implements AttributeMatcher { + private final String[] mKeys; + + MultiKeyMatcher(List keys) { + mKeys = new String[keys.size()]; + for (int i = 0, n = mKeys.length; i < n; ++i) { + mKeys[i] = keys.get(i).intern(); + } + } + + @Override + public boolean isCoveredBy(AttributeMatcher attributeMatcher) { + if (attributeMatcher == this) { + return true; + } + + Tag[] tags = new Tag[mKeys.length]; + int i = 0; + for (String key : mKeys) { + tags[i++] = new Tag(key, null); + } + return attributeMatcher.matches(tags); + } + + @Override + public boolean matches(Tag[] tags) { + for (Tag tag : tags) + for (String key : mKeys) + if (key == tag.key) + return true; + + return false; + } +} diff --git a/src/org/mapsforge/android/rendertheme/MultiValueMatcher.java b/src/org/mapsforge/android/rendertheme/MultiValueMatcher.java new file mode 100644 index 00000000..e5b25a5a --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/MultiValueMatcher.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.util.List; + +import org.mapsforge.core.Tag; + +class MultiValueMatcher implements AttributeMatcher { + private final String[] mValues; + + MultiValueMatcher(List values) { + mValues = new String[values.size()]; + for (int i = 0, n = mValues.length; i < n; ++i) { + mValues[i] = values.get(i).intern(); + } + } + + @Override + public boolean isCoveredBy(AttributeMatcher attributeMatcher) { + if (attributeMatcher == this) { + return true; + } + Tag[] tags = new Tag[mValues.length]; + + int i = 0; + for (String val : mValues) { + tags[i++] = new Tag(null, val); + } + return attributeMatcher.matches(tags); + } + + @Override + public boolean matches(Tag[] tags) { + for (Tag tag : tags) + for (String val : mValues) + if (val == tag.value) + return true; + + return false; + } +} diff --git a/src/org/mapsforge/android/rendertheme/NegativeMatcher.java b/src/org/mapsforge/android/rendertheme/NegativeMatcher.java new file mode 100644 index 00000000..1ef5fb3f --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/NegativeMatcher.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.util.List; + +import org.mapsforge.core.Tag; + +class NegativeMatcher implements AttributeMatcher { + private final String[] mKeyList; + private final String[] mValueList; + + NegativeMatcher(List keyList, List valueList) { + mKeyList = new String[keyList.size()]; + for (int i = 0; i < mKeyList.length; i++) + mKeyList[i] = keyList.get(i).intern(); + + mValueList = new String[valueList.size()]; + for (int i = 0; i < mValueList.length; i++) + mValueList[i] = valueList.get(i).intern(); + } + + @Override + public boolean isCoveredBy(AttributeMatcher attributeMatcher) { + return false; + } + + @Override + public boolean matches(Tag[] tags) { + if (keyListDoesNotContainKeys(tags)) { + return true; + } + + for (Tag tag : tags) { + for (String value : mValueList) + if (value == tag.value) + return true; + } + return false; + } + + private boolean keyListDoesNotContainKeys(Tag[] tags) { + for (Tag tag : tags) { + for (String key : mKeyList) + if (key == tag.key) + return false; + + } + return true; + } +} diff --git a/src/org/mapsforge/android/rendertheme/NegativeRule.java b/src/org/mapsforge/android/rendertheme/NegativeRule.java new file mode 100644 index 00000000..0eb56942 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/NegativeRule.java @@ -0,0 +1,42 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import org.mapsforge.core.Tag; + +class NegativeRule extends Rule { + final AttributeMatcher mAttributeMatcher; + + NegativeRule(ElementMatcher elementMatcher, ClosedMatcher closedMatcher, byte zoomMin, byte zoomMax, + AttributeMatcher attributeMatcher) { + super(elementMatcher, closedMatcher, zoomMin, zoomMax); + + mAttributeMatcher = attributeMatcher; + } + + @Override + boolean matchesNode(Tag[] tags, byte zoomLevel) { + return mZoomMin <= zoomLevel && mZoomMax >= zoomLevel + && (mElementMatcher == null || mElementMatcher.matches(Element.NODE)) + && mAttributeMatcher.matches(tags); + } + + @Override + boolean matchesWay(Tag[] tags, byte zoomLevel, Closed closed) { + return mZoomMin <= zoomLevel && mZoomMax >= zoomLevel + && (mElementMatcher == null || mElementMatcher.matches(Element.WAY)) + && (mClosedMatcher == null || mClosedMatcher.matches(closed)) && mAttributeMatcher.matches(tags); + } +} diff --git a/src/org/mapsforge/android/rendertheme/PositiveRule.java b/src/org/mapsforge/android/rendertheme/PositiveRule.java new file mode 100644 index 00000000..96c13ad1 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/PositiveRule.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import org.mapsforge.core.Tag; + +class PositiveRule extends Rule { + final AttributeMatcher mKeyMatcher; + final AttributeMatcher mValueMatcher; + + PositiveRule(ElementMatcher elementMatcher, ClosedMatcher closedMatcher, byte zoomMin, byte zoomMax, + AttributeMatcher keyMatcher, AttributeMatcher valueMatcher) { + super(elementMatcher, closedMatcher, zoomMin, zoomMax); + + if (keyMatcher instanceof AnyMatcher) + mKeyMatcher = null; + else + mKeyMatcher = keyMatcher; + + if (valueMatcher instanceof AnyMatcher) + mValueMatcher = null; + else + mValueMatcher = valueMatcher; + } + + @Override + boolean matchesNode(Tag[] tags, byte zoomLevel) { + return mZoomMin <= zoomLevel && mZoomMax >= zoomLevel + && (mElementMatcher == null || mElementMatcher.matches(Element.NODE)) + && (mKeyMatcher == null || mKeyMatcher.matches(tags)) + && (mValueMatcher == null || mValueMatcher.matches(tags)); + } + + @Override + boolean matchesWay(Tag[] tags, byte zoomLevel, Closed closed) { + return mZoomMin <= zoomLevel && mZoomMax >= zoomLevel + && (mElementMatcher == null || mElementMatcher.matches(Element.WAY)) + && (mClosedMatcher == null || mClosedMatcher.matches(closed)) + && (mKeyMatcher == null || mKeyMatcher.matches(tags)) + && (mValueMatcher == null || mValueMatcher.matches(tags)); + } +} diff --git a/src/org/mapsforge/android/rendertheme/RenderCallback.java b/src/org/mapsforge/android/rendertheme/RenderCallback.java new file mode 100644 index 00000000..d49e3976 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/RenderCallback.java @@ -0,0 +1,122 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import org.mapsforge.android.rendertheme.renderinstruction.Area; +import org.mapsforge.android.rendertheme.renderinstruction.Line; + +import android.graphics.Bitmap; +import android.graphics.Paint; + +/** + * Callback methods for rendering areas, ways and points of interest (POIs). + */ +public interface RenderCallback { + /** + * Renders an area with the given parameters. + * + * @param area + * ... + */ + void renderArea(Area area); + + /** + * Renders an area caption with the given text. + * + * @param caption + * the text to be rendered. + * @param verticalOffset + * the vertical offset of the caption. + * @param paint + * the paint to be used for rendering the text. + * @param stroke + * an optional paint for the text casing (may be null). + */ + void renderAreaCaption(String caption, float verticalOffset, Paint paint, Paint stroke); + + /** + * Renders an area symbol with the given bitmap. + * + * @param symbol + * the symbol to be rendered. + */ + void renderAreaSymbol(Bitmap symbol); + + /** + * Renders a point of interest caption with the given text. + * + * @param caption + * the text to be rendered. + * @param verticalOffset + * the vertical offset of the caption. + * @param paint + * the paint to be used for rendering the text. + * @param stroke + * an optional paint for the text casing (may be null). + */ + void renderPointOfInterestCaption(String caption, float verticalOffset, Paint paint, Paint stroke); + + /** + * Renders a point of interest circle with the given parameters. + * + * @param radius + * the radius of the circle. + * @param fill + * the paint to be used for rendering the circle. + * @param level + * the drawing level on which the circle should be rendered. + */ + void renderPointOfInterestCircle(float radius, Paint fill, int level); + + /** + * Renders a point of interest symbol with the given bitmap. + * + * @param symbol + * the symbol to be rendered. + */ + void renderPointOfInterestSymbol(Bitmap symbol); + + /** + * Renders a way with the given parameters. + * + * @param line + * ... + */ + void renderWay(Line line); + + /** + * Renders a way with the given symbol along the way path. + * + * @param symbol + * the symbol to be rendered. + * @param alignCenter + * true if the symbol should be centered, false otherwise. + * @param repeat + * true if the symbol should be repeated, false otherwise. + */ + void renderWaySymbol(Bitmap symbol, boolean alignCenter, boolean repeat); + + /** + * Renders a way with the given text along the way path. + * + * @param text + * the text to be rendered. + * @param paint + * the paint to be used for rendering the text. + * @param stroke + * an optional paint for the text casing (may be null). + */ + void renderWayText(String text, Paint paint, Paint stroke); +} diff --git a/src/org/mapsforge/android/rendertheme/RenderTheme.java b/src/org/mapsforge/android/rendertheme/RenderTheme.java new file mode 100644 index 00000000..04f10fdc --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/RenderTheme.java @@ -0,0 +1,377 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.util.ArrayList; +import java.util.List; + +import org.mapsforge.android.rendertheme.renderinstruction.Line; +import org.mapsforge.android.rendertheme.renderinstruction.RenderInstruction; +import org.mapsforge.core.LRUCache; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +import android.graphics.Color; + +/** + * A RenderTheme defines how ways and nodes are drawn. + */ +public class RenderTheme { + private static final int MATCHING_CACHE_SIZE = 1024; + private static final int RENDER_THEME_VERSION = 1; + + private static void validate(String elementName, Integer version, float baseStrokeWidth, float baseTextSize) { + if (version == null) { + throw new IllegalArgumentException("missing attribute version for element:" + elementName); + } else if (version.intValue() != RENDER_THEME_VERSION) { + throw new IllegalArgumentException("invalid render theme version:" + version); + } else if (baseStrokeWidth < 0) { + throw new IllegalArgumentException("base-stroke-width must not be negative: " + baseStrokeWidth); + } else if (baseTextSize < 0) { + throw new IllegalArgumentException("base-text-size must not be negative: " + baseTextSize); + } + } + + static RenderTheme create(String elementName, Attributes attributes) { + Integer version = null; + int mapBackground = Color.WHITE; + float baseStrokeWidth = 1; + float baseTextSize = 1; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("schemaLocation".equals(name)) { + continue; + } else if ("version".equals(name)) { + version = Integer.valueOf(Integer.parseInt(value)); + } else if ("map-background".equals(name)) { + mapBackground = Color.parseColor(value); + } else if ("base-stroke-width".equals(name)) { + baseStrokeWidth = Float.parseFloat(value); + } else if ("base-text-size".equals(name)) { + baseTextSize = Float.parseFloat(value); + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(elementName, version, baseStrokeWidth, baseTextSize); + return new RenderTheme(mapBackground, baseStrokeWidth, baseTextSize); + } + + private final float mBaseStrokeWidth; + private final float mBaseTextSize; + private int mLevels; + private final int mMapBackground; + private final ArrayList mRulesList; + + private final LRUCache mMatchingCacheNodes; + private final LRUCache mMatchingCacheWay; + private final LRUCache mMatchingCacheArea; + + // private List mMatchingListWay; + // private List mMatchingListArea; + // private List mMatchingListNode; + + RenderTheme(int mapBackground, float baseStrokeWidth, float baseTextSize) { + mMapBackground = mapBackground; + mBaseStrokeWidth = baseStrokeWidth; + mBaseTextSize = baseTextSize; + mRulesList = new ArrayList(); + + mMatchingCacheNodes = new LRUCache(MATCHING_CACHE_SIZE); + mMatchingCacheWay = new LRUCache(MATCHING_CACHE_SIZE); + mMatchingCacheArea = new LRUCache(MATCHING_CACHE_SIZE); + } + + /** + * Must be called when this RenderTheme gets destroyed to clean up and free resources. + */ + public void destroy() { + mMatchingCacheNodes.clear(); + mMatchingCacheArea.clear(); + mMatchingCacheWay.clear(); + + for (int i = 0, n = mRulesList.size(); i < n; ++i) { + mRulesList.get(i).onDestroy(); + } + } + + /** + * @return the number of distinct drawing levels required by this RenderTheme. + */ + public int getLevels() { + return mLevels; + } + + /** + * @return the map background color of this RenderTheme. + * @see Color + */ + public int getMapBackground() { + return mMapBackground; + } + + /** + * @param renderCallback + * ... + * @param tags + * ... + * @param zoomLevel + * ... + */ + public void matchNode(RenderCallback renderCallback, Tag[] tags, byte zoomLevel) { + // List matchingList = matchingListNode; + // MatchingCacheKey matchingCacheKey = matchingCacheKeyNode; + // + // if (!changed) { + // if (matchingList != null) { + // for (int i = 0, n = matchingList.size(); i < n; ++i) { + // matchingList.get(i).renderNode(renderCallback, tags); + // } + // } + // return; + // } + // matchingCacheKey = new MatchingCacheKey(tags, zoomLevel); + // matchingList = matchingCacheNodes.get(matchingCacheKey); + // + // if (matchingList != null) { + // // cache hit + // for (int i = 0, n = matchingList.size(); i < n; ++i) { + // matchingList.get(i).renderNode(renderCallback, tags); + // } + // } else { + // // cache miss + // matchingList = new ArrayList(); + // for (int i = 0, n = mRulesList.size(); i < n; ++i) { + // mRulesList.get(i).matchNode(renderCallback, tags, zoomLevel, matchingList); + // } + // matchingCacheNodes.put(matchingCacheKey, matchingList); + // } + // + // matchingListNode = matchingList; + // matchingCacheKeyNode = matchingCacheKey; + } + + /** + * Scales the stroke width of this RenderTheme by the given factor. + * + * @param scaleFactor + * the factor by which the stroke width should be scaled. + */ + public void scaleStrokeWidth(float scaleFactor) { + for (int i = 0, n = mRulesList.size(); i < n; ++i) { + mRulesList.get(i).scaleStrokeWidth(scaleFactor * mBaseStrokeWidth); + } + } + + /** + * Scales the text size of this RenderTheme by the given factor. + * + * @param scaleFactor + * the factor by which the text size should be scaled. + */ + public void scaleTextSize(float scaleFactor) { + for (int i = 0, n = mRulesList.size(); i < n; ++i) { + mRulesList.get(i).scaleTextSize(scaleFactor * mBaseTextSize); + } + } + + private RenderInstruction[] mRenderInstructions = null; + + /** + * Matches a way with the given parameters against this RenderTheme. + * + * @param renderCallback + * the callback implementation which will be executed on each match. + * @param tags + * the tags of the way. + * @param zoomLevel + * the zoom level at which the way should be matched. + * @param closed + * way is Closed + * @param changed + * ... + */ + public void matchWay(RenderCallback renderCallback, Tag[] tags, byte zoomLevel, boolean closed, boolean changed) { + RenderInstruction[] renderInstructions = null; + + LRUCache matchingCache; + MatchingCacheKey matchingCacheKey; + + if (!changed) { + renderInstructions = mRenderInstructions; + + if (renderInstructions != null) { + for (int i = 0, n = renderInstructions.length; i < n; i++) + renderInstructions[i].renderWay(renderCallback, tags); + } + return; + } + + if (closed) { + matchingCache = mMatchingCacheArea; + } else { + matchingCache = mMatchingCacheWay; + } + + matchingCacheKey = new MatchingCacheKey(tags, zoomLevel); + boolean found = matchingCache.containsKey(matchingCacheKey); + if (found) { + renderInstructions = matchingCache.get(matchingCacheKey); + + if (renderInstructions != null) { + for (int i = 0, n = renderInstructions.length; i < n; i++) + renderInstructions[i].renderWay(renderCallback, tags); + + } + } else { + // cache miss + Closed c = (closed ? Closed.YES : Closed.NO); + List matchingList = new ArrayList(4); + for (int i = 0, n = mRulesList.size(); i < n; ++i) { + mRulesList.get(i).matchWay(renderCallback, tags, zoomLevel, c, matchingList); + } + int size = matchingList.size(); + if (size > 0) { + renderInstructions = new RenderInstruction[matchingList.size()]; + for (int i = 0, n = matchingList.size(); i < n; ++i) { + RenderInstruction renderInstruction = matchingList.get(i); + renderInstruction.renderWay(renderCallback, tags); + renderInstructions[i] = renderInstruction; + } + } + matchingCache.put(matchingCacheKey, renderInstructions); + } + + mRenderInstructions = renderInstructions; + + // if (matchingList != null) { + // for (int i = 0, n = matchingList.size(); i < n; ++i) { + // matchingList.get(i).renderWay(renderCallback, tags); + // } + // } + // return renderInstructions; + + // if (closed) { + // mMatchingListArea = matchingList; + // } else { + // mMatchingListWay = matchingList; + // } + + // if (mCompareKey.set(tags, zoomLevel, closed)) { + // // Log.d("mapsforge", "SAME AS BAFORE!!!" + tags); + // for (int i = 0, n = mInstructionList.length; i < n; ++i) { + // mInstructionList[i].renderWay(renderCallback, tags); + // } + // return; + // } + // + // SparseArray matchingList = mMatchingCache.get(mCompareKey); + // + // if (matchingList != null) { + // mInstructionList = matchingList.get(zoomLevel); + // if (mInstructionList != null) { + // // cache hit + // // Log.d("mapsforge", "CCACHE HIT !!!" + tags); + // for (int i = 0, n = mInstructionList.length; i < n; ++i) { + // mInstructionList[i].renderWay(renderCallback, tags); + // } + // + // return; + // } + // } + // // Log.d("mapsforge", "CACHE MISS !!!" + tags); + // // cache miss + // ArrayList instructionList = new ArrayList(); + // + // for (int i = 0, n = mRulesList.size(); i < n; ++i) { + // mRulesList.get(i).matchWay(renderCallback, mCompareKey.getTags(), zoomLevel, closed, instructionList); + // } + // + // boolean found = false; + // int size = instructionList.size(); + // + // if (matchingList == null) { + // matchingList = new SparseArray(25); + // MatchingCacheKey matchingCacheKey = new MatchingCacheKey(mCompareKey); + // mMatchingCache.put(matchingCacheKey, matchingList); + // } else { + // // check if another zoomLevel uses the same instructionList + // for (int i = 0, n = matchingList.size(); i < n; i++) { + // int key = matchingList.keyAt(i); + // + // RenderInstruction[] list2 = matchingList.get(key); + // if (list2.length != size) + // continue; + // + // int j = 0; + // while (j < size && (list2[j] == instructionList.get(j))) + // j++; + // + // if (j == size) { + // instructionList.clear(); + // mInstructionList = list2; + // found = true; + // break; + // } + // } + // } + // + // if (!found) { + // mInstructionList = new RenderInstruction[size]; + // for (int i = 0; i < size; i++) + // mInstructionList[i] = instructionList.get(i); + // } + // + // for (int i = 0, n = mInstructionList.length; i < n; ++i) + // mInstructionList[i].renderWay(renderCallback, tags); + // + // matchingList.put(zoomLevel, mInstructionList); + } + + void addRule(Rule rule) { + mRulesList.add(rule); + } + + void complete() { + mRulesList.trimToSize(); + for (int i = 0, n = mRulesList.size(); i < n; ++i) { + mRulesList.get(i).onComplete(); + } + + } + + private final ArrayList outlineLayers = new ArrayList(); + + void addOutlineLayer(Line line) { + outlineLayers.add(line); + } + + /** + * @param layer + * ... + * @return Line (paint and level) used for outline + */ + public Line getOutline(int layer) { + return outlineLayers.get(layer); + } + + void setLevels(int levels) { + mLevels = levels; + } +} diff --git a/src/org/mapsforge/android/rendertheme/RenderThemeHandler.java b/src/org/mapsforge/android/rendertheme/RenderThemeHandler.java new file mode 100644 index 00000000..4b615ca4 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/RenderThemeHandler.java @@ -0,0 +1,245 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Stack; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + +import org.mapsforge.android.rendertheme.renderinstruction.Area; +import org.mapsforge.android.rendertheme.renderinstruction.Caption; +import org.mapsforge.android.rendertheme.renderinstruction.Circle; +import org.mapsforge.android.rendertheme.renderinstruction.Line; +import org.mapsforge.android.rendertheme.renderinstruction.LineSymbol; +import org.mapsforge.android.rendertheme.renderinstruction.PathText; +import org.mapsforge.android.rendertheme.renderinstruction.Symbol; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +/** + * SAX2 handler to parse XML render theme files. + */ +public class RenderThemeHandler extends DefaultHandler { + private static final Logger LOG = Logger.getLogger(RenderThemeHandler.class.getName()); + + private static enum Element { + RENDER_THEME, RENDERING_INSTRUCTION, RULE; + } + + private static final String ELEMENT_NAME_RENDER_THEME = "rendertheme"; + private static final String ELEMENT_NAME_RULE = "rule"; + private static final String UNEXPECTED_ELEMENT = "unexpected element: "; + + /** + * @param inputStream + * an input stream containing valid render theme XML data. + * @return a new RenderTheme which is created by parsing the XML data from the input stream. + * @throws SAXException + * if an error occurs while parsing the render theme XML. + * @throws ParserConfigurationException + * if an error occurs while creating the XML parser. + * @throws IOException + * if an I/O error occurs while reading from the input stream. + */ + public static RenderTheme getRenderTheme(InputStream inputStream) throws SAXException, + ParserConfigurationException, IOException { + RenderThemeHandler renderThemeHandler = new RenderThemeHandler(); + XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); + xmlReader.setContentHandler(renderThemeHandler); + xmlReader.parse(new InputSource(inputStream)); + return renderThemeHandler.mRenderTheme; + } + + /** + * Logs the given information about an unknown XML attribute. + * + * @param element + * the XML element name. + * @param name + * the XML attribute name. + * @param value + * the XML attribute value. + * @param attributeIndex + * the XML attribute index position. + */ + public static void logUnknownAttribute(String element, String name, String value, int attributeIndex) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("unknown attribute in element "); + stringBuilder.append(element); + stringBuilder.append(" ("); + stringBuilder.append(attributeIndex); + stringBuilder.append("): "); + stringBuilder.append(name); + stringBuilder.append('='); + stringBuilder.append(value); + LOG.info(stringBuilder.toString()); + } + + private Rule mCurrentRule; + private final Stack mElementStack = new Stack(); + private int mLevel; + private RenderTheme mRenderTheme; + private final Stack mRuleStack = new Stack(); + + @Override + public void endDocument() { + if (mRenderTheme == null) { + throw new IllegalArgumentException("missing element: rules"); + } + + mRenderTheme.setLevels(mLevel); + mRenderTheme.complete(); + } + + @Override + public void endElement(String uri, String localName, String qName) { + mElementStack.pop(); + + if (ELEMENT_NAME_RULE.equals(localName)) { + mRuleStack.pop(); + if (mRuleStack.empty()) { + mRenderTheme.addRule(mCurrentRule); + } else { + mCurrentRule = mRuleStack.peek(); + } + } + } + + @Override + public void error(SAXParseException exception) { + LOG.log(Level.SEVERE, null, exception); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + try { + if (ELEMENT_NAME_RENDER_THEME.equals(localName)) { + checkState(localName, Element.RENDER_THEME); + mRenderTheme = RenderTheme.create(localName, attributes); + } + + else if (ELEMENT_NAME_RULE.equals(localName)) { + checkState(localName, Element.RULE); + Rule rule = Rule.create(localName, attributes, mRuleStack); + if (!mRuleStack.empty()) { + mCurrentRule.addSubRule(rule); + } + mCurrentRule = rule; + mRuleStack.push(mCurrentRule); + } + + else if ("area".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + Area area = Area.create(localName, attributes, mLevel++); + mRuleStack.peek().addRenderingInstruction(area); + } + + else if ("caption".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + Caption caption = Caption.create(localName, attributes); + mCurrentRule.addRenderingInstruction(caption); + } + + else if ("circle".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + Circle circle = Circle.create(localName, attributes, mLevel++); + mCurrentRule.addRenderingInstruction(circle); + } + + else if ("line".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + Line line = Line.create(localName, attributes, mLevel++); + mCurrentRule.addRenderingInstruction(line); + } + + else if ("outline".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + Line line = Line.create(localName, attributes, mLevel++); + mRenderTheme.addOutlineLayer(line); + // mCurrentRule.addRenderingInstruction(line); + } + + else if ("lineSymbol".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + LineSymbol lineSymbol = LineSymbol.create(localName, attributes); + mCurrentRule.addRenderingInstruction(lineSymbol); + } + + else if ("pathText".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + PathText pathText = PathText.create(localName, attributes); + mCurrentRule.addRenderingInstruction(pathText); + } + + else if ("symbol".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + Symbol symbol = Symbol.create(localName, attributes); + mCurrentRule.addRenderingInstruction(symbol); + } + + else { + throw new SAXException("unknown element: " + localName); + } + } catch (IllegalArgumentException e) { + throw new SAXException(null, e); + } catch (IOException e) { + throw new SAXException(null, e); + } + } + + @Override + public void warning(SAXParseException exception) { + LOG.log(Level.SEVERE, null, exception); + } + + private void checkElement(String elementName, Element element) throws SAXException { + switch (element) { + case RENDER_THEME: + if (!mElementStack.empty()) { + throw new SAXException(UNEXPECTED_ELEMENT + elementName); + } + return; + + case RULE: + Element parentElement = mElementStack.peek(); + if (parentElement != Element.RENDER_THEME && parentElement != Element.RULE) { + throw new SAXException(UNEXPECTED_ELEMENT + elementName); + } + return; + + case RENDERING_INSTRUCTION: + if (mElementStack.peek() != Element.RULE) { + throw new SAXException(UNEXPECTED_ELEMENT + elementName); + } + return; + } + + throw new SAXException("unknown enum value: " + element); + } + + private void checkState(String elementName, Element element) throws SAXException { + checkElement(elementName, element); + mElementStack.push(element); + } +} diff --git a/src/org/mapsforge/android/rendertheme/Rule.java b/src/org/mapsforge/android/rendertheme/Rule.java new file mode 100644 index 00000000..04ef1a25 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/Rule.java @@ -0,0 +1,287 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Pattern; + +import org.mapsforge.android.rendertheme.renderinstruction.RenderInstruction; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +abstract class Rule { + private static final Map, AttributeMatcher> MATCHERS_CACHE_KEY = new HashMap, AttributeMatcher>(); + private static final Map, AttributeMatcher> MATCHERS_CACHE_VALUE = new HashMap, AttributeMatcher>(); + private static final Pattern SPLIT_PATTERN = Pattern.compile("\\|"); + private static final String STRING_NEGATION = "~"; + private static final String STRING_WILDCARD = "*"; + private static final String UNKNOWN_ENUM_VALUE = "unknown enum value: "; + + private static Rule createRule(Stack ruleStack, Element element, String keys, String values, Closed closed, + byte zoomMin, byte zoomMax) { + ElementMatcher elementMatcher = getElementMatcher(element); + ClosedMatcher closedMatcher = getClosedMatcher(closed); + List keyList = new ArrayList(Arrays.asList(SPLIT_PATTERN.split(keys))); + List valueList = new ArrayList(Arrays.asList(SPLIT_PATTERN.split(values))); + + elementMatcher = RuleOptimizer.optimize(elementMatcher, ruleStack); + closedMatcher = RuleOptimizer.optimize(closedMatcher, ruleStack); + + if (valueList.remove(STRING_NEGATION)) { + AttributeMatcher attributeMatcher = new NegativeMatcher(keyList, valueList); + return new NegativeRule(elementMatcher, closedMatcher, zoomMin, zoomMax, attributeMatcher); + } + + AttributeMatcher keyMatcher = getKeyMatcher(keyList); + AttributeMatcher valueMatcher = getValueMatcher(valueList); + + keyMatcher = RuleOptimizer.optimize(keyMatcher, ruleStack); + valueMatcher = RuleOptimizer.optimize(valueMatcher, ruleStack); + + return new PositiveRule(elementMatcher, closedMatcher, zoomMin, zoomMax, keyMatcher, valueMatcher); + } + + private static ClosedMatcher getClosedMatcher(Closed closed) { + switch (closed) { + case YES: + return ClosedWayMatcher.getInstance(); + case NO: + return LinearWayMatcher.getInstance(); + case ANY: + return AnyMatcher.getInstance(); + } + + throw new IllegalArgumentException(UNKNOWN_ENUM_VALUE + closed); + } + + private static ElementMatcher getElementMatcher(Element element) { + switch (element) { + case NODE: + return ElementNodeMatcher.getInstance(); + case WAY: + return ElementWayMatcher.getInstance(); + case ANY: + return AnyMatcher.getInstance(); + } + + throw new IllegalArgumentException(UNKNOWN_ENUM_VALUE + element); + } + + private static AttributeMatcher getKeyMatcher(List keyList) { + if (STRING_WILDCARD.equals(keyList.get(0))) { + return AnyMatcher.getInstance(); + } + + AttributeMatcher attributeMatcher = MATCHERS_CACHE_KEY.get(keyList); + if (attributeMatcher == null) { + if (keyList.size() == 1) { + attributeMatcher = new SingleKeyMatcher(keyList.get(0)); + } else { + attributeMatcher = new MultiKeyMatcher(keyList); + } + MATCHERS_CACHE_KEY.put(keyList, attributeMatcher); + } + return attributeMatcher; + } + + private static AttributeMatcher getValueMatcher(List valueList) { + if (STRING_WILDCARD.equals(valueList.get(0))) { + return AnyMatcher.getInstance(); + } + + AttributeMatcher attributeMatcher = MATCHERS_CACHE_VALUE.get(valueList); + if (attributeMatcher == null) { + if (valueList.size() == 1) { + attributeMatcher = new SingleValueMatcher(valueList.get(0)); + } else { + attributeMatcher = new MultiValueMatcher(valueList); + } + MATCHERS_CACHE_VALUE.put(valueList, attributeMatcher); + } + return attributeMatcher; + } + + private static void validate(String elementName, Element element, String keys, String values, byte zoomMin, + byte zoomMax) { + if (element == null) { + throw new IllegalArgumentException("missing attribute e for element: " + elementName); + } else if (keys == null) { + throw new IllegalArgumentException("missing attribute k for element: " + elementName); + } else if (values == null) { + throw new IllegalArgumentException("missing attribute v for element: " + elementName); + } else if (zoomMin < 0) { + throw new IllegalArgumentException("zoom-min must not be negative: " + zoomMin); + } else if (zoomMax < 0) { + throw new IllegalArgumentException("zoom-max must not be negative: " + zoomMax); + } else if (zoomMin > zoomMax) { + throw new IllegalArgumentException("zoom-min must be less or equal zoom-max: " + zoomMin); + } + } + + static Rule create(String elementName, Attributes attributes, Stack ruleStack) { + Element element = null; + String keys = null; + String values = null; + Closed closed = Closed.ANY; + byte zoomMin = 0; + byte zoomMax = Byte.MAX_VALUE; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("e".equals(name)) { + element = Element.valueOf(value.toUpperCase(Locale.ENGLISH)); + } else if ("k".equals(name)) { + keys = value; + } else if ("v".equals(name)) { + values = value; + } else if ("closed".equals(name)) { + closed = Closed.valueOf(value.toUpperCase(Locale.ENGLISH)); + } else if ("zoom-min".equals(name)) { + zoomMin = Byte.parseByte(value); + } else if ("zoom-max".equals(name)) { + zoomMax = Byte.parseByte(value); + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(elementName, element, keys, values, zoomMin, zoomMax); + return createRule(ruleStack, element, keys, values, closed, zoomMin, zoomMax); + } + + private ArrayList mRenderInstructions; + private ArrayList mSubRules; + + private Rule[] mSubRuleArray; + private RenderInstruction[] mRenderInstructionArray; + + final ClosedMatcher mClosedMatcher; + final ElementMatcher mElementMatcher; + final byte mZoomMax; + final byte mZoomMin; + + Rule(ElementMatcher elementMatcher, ClosedMatcher closedMatcher, byte zoomMin, byte zoomMax) { + if (elementMatcher instanceof AnyMatcher) + mElementMatcher = null; + else + mElementMatcher = elementMatcher; + + if (closedMatcher instanceof AnyMatcher) + mClosedMatcher = null; + else + mClosedMatcher = closedMatcher; + + mZoomMin = zoomMin; + mZoomMax = zoomMax; + + mRenderInstructions = new ArrayList(4); + mSubRules = new ArrayList(4); + } + + void addRenderingInstruction(RenderInstruction renderInstruction) { + mRenderInstructions.add(renderInstruction); + } + + void addSubRule(Rule rule) { + mSubRules.add(rule); + } + + abstract boolean matchesNode(Tag[] tags, byte zoomLevel); + + abstract boolean matchesWay(Tag[] tags, byte zoomLevel, Closed closed); + + void matchNode(RenderCallback renderCallback, Tag[] tags, byte zoomLevel) { + if (matchesNode(tags, zoomLevel)) { + for (int i = 0, n = mRenderInstructionArray.length; i < n; i++) + mRenderInstructionArray[i].renderNode(renderCallback, tags); + + for (int i = 0, n = mSubRuleArray.length; i < n; i++) + mSubRuleArray[i].matchNode(renderCallback, tags, zoomLevel); + + } + } + + void matchWay(RenderCallback renderCallback, Tag[] tags, byte zoomLevel, Closed closed, + List matchingList) { + + if (matchesWay(tags, zoomLevel, closed)) { + for (int i = 0, n = mRenderInstructionArray.length; i < n; i++) + matchingList.add(mRenderInstructionArray[i]); + + for (int i = 0, n = mSubRuleArray.length; i < n; i++) + mSubRuleArray[i].matchWay(renderCallback, tags, zoomLevel, closed, matchingList); + + } + } + + void onComplete() { + MATCHERS_CACHE_KEY.clear(); + MATCHERS_CACHE_VALUE.clear(); + + mRenderInstructionArray = new RenderInstruction[mRenderInstructions.size()]; + + for (int i = 0, n = mRenderInstructions.size(); i < n; i++) + mRenderInstructionArray[i] = mRenderInstructions.get(i); + + mSubRuleArray = new Rule[mSubRules.size()]; + + for (int i = 0, n = mSubRules.size(); i < n; i++) + mSubRuleArray[i] = mSubRules.get(i); + + mRenderInstructions.clear(); + mRenderInstructions = null; + mSubRules.clear(); + mSubRules = null; + + for (int i = 0, n = mSubRuleArray.length; i < n; i++) + mSubRuleArray[i].onComplete(); + + } + + void onDestroy() { + for (int i = 0, n = mRenderInstructionArray.length; i < n; i++) + mRenderInstructionArray[i].destroy(); + + for (int i = 0, n = mSubRuleArray.length; i < n; i++) + mSubRuleArray[i].onDestroy(); + + } + + void scaleStrokeWidth(float scaleFactor) { + for (int i = 0, n = mRenderInstructionArray.length; i < n; i++) + mRenderInstructionArray[i].scaleStrokeWidth(scaleFactor); + + for (int i = 0, n = mSubRuleArray.length; i < n; i++) + mSubRuleArray[i].scaleStrokeWidth(scaleFactor); + + } + + void scaleTextSize(float scaleFactor) { + for (int i = 0, n = mRenderInstructionArray.length; i < n; i++) + mRenderInstructionArray[i].scaleTextSize(scaleFactor); + + for (int i = 0, n = mSubRuleArray.length; i < n; i++) + mSubRuleArray[i].scaleTextSize(scaleFactor); + + } +} diff --git a/src/org/mapsforge/android/rendertheme/RuleOptimizer.java b/src/org/mapsforge/android/rendertheme/RuleOptimizer.java new file mode 100644 index 00000000..e8b85646 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/RuleOptimizer.java @@ -0,0 +1,125 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import java.util.Stack; +import java.util.logging.Logger; + +final class RuleOptimizer { + private static final Logger LOG = Logger.getLogger(RuleOptimizer.class.getName()); + + private static AttributeMatcher optimizeKeyMatcher(AttributeMatcher attributeMatcher, Stack ruleStack) { + for (int i = 0, n = ruleStack.size(); i < n; ++i) { + if (ruleStack.get(i) instanceof PositiveRule) { + PositiveRule positiveRule = (PositiveRule) ruleStack.get(i); + if (positiveRule.mKeyMatcher != null && positiveRule.mKeyMatcher.isCoveredBy(attributeMatcher)) { + return null; // AnyMatcher.getInstance(); + } + } + } + + return attributeMatcher; + } + + private static AttributeMatcher optimizeValueMatcher(AttributeMatcher attributeMatcher, Stack ruleStack) { + for (int i = 0, n = ruleStack.size(); i < n; ++i) { + if (ruleStack.get(i) instanceof PositiveRule) { + PositiveRule positiveRule = (PositiveRule) ruleStack.get(i); + + if (positiveRule.mValueMatcher != null && positiveRule.mValueMatcher.isCoveredBy(attributeMatcher)) { + return null; // AnyMatcher.getInstance(); + } + } + } + + return attributeMatcher; + } + + static AttributeMatcher optimize(AttributeMatcher attributeMatcher, Stack ruleStack) { + if (attributeMatcher instanceof AnyMatcher) + return attributeMatcher;// return null; + else if (attributeMatcher instanceof NegativeMatcher) { + return attributeMatcher; + } else if (attributeMatcher instanceof SingleKeyMatcher) { + return optimizeKeyMatcher(attributeMatcher, ruleStack); + } else if (attributeMatcher instanceof SingleValueMatcher) { + return optimizeValueMatcher(attributeMatcher, ruleStack); + } else if (attributeMatcher instanceof MultiKeyMatcher) { + return optimizeKeyMatcher(attributeMatcher, ruleStack); + } else if (attributeMatcher instanceof MultiValueMatcher) { + return optimizeValueMatcher(attributeMatcher, ruleStack); + } + throw new IllegalArgumentException("unknown AttributeMatcher: " + attributeMatcher); + } + + static ClosedMatcher optimize(ClosedMatcher closedMatcher, Stack ruleStack) { + if (closedMatcher == null) { + return null; + } + + if (closedMatcher instanceof AnyMatcher) { + return null; + } + + for (int i = 0, n = ruleStack.size(); i < n; ++i) { + ClosedMatcher matcher = ruleStack.get(i).mClosedMatcher; + if (matcher == null) + return null; + + if (matcher.isCoveredBy(closedMatcher)) { + return null; // AnyMatcher.getInstance(); + + } else if (!closedMatcher.isCoveredBy(ruleStack.get(i).mClosedMatcher)) { + LOG.warning("unreachable rule (closed)"); + } + } + + return closedMatcher; + } + + static ElementMatcher optimize(ElementMatcher elementMatcher, Stack ruleStack) { + + if (elementMatcher == null) { + return null; + } + + if (elementMatcher instanceof AnyMatcher) { + return null; + } + + for (int i = 0, n = ruleStack.size(); i < n; ++i) { + ElementMatcher matcher = ruleStack.get(i).mElementMatcher; + + if (matcher == null) + return null; + + if (matcher.isCoveredBy(elementMatcher)) { + return null; // AnyMatcher.getInstance(); + + } else if (!elementMatcher.isCoveredBy(ruleStack.get(i).mElementMatcher)) { + LOG.warning("unreachable rule (e)"); + } + } + + return elementMatcher; + } + + /** + * Private constructor to prevent instantiation from other classes. + */ + private RuleOptimizer() { + // do nothing + } +} diff --git a/src/org/mapsforge/android/rendertheme/SingleKeyMatcher.java b/src/org/mapsforge/android/rendertheme/SingleKeyMatcher.java new file mode 100644 index 00000000..4240c1f0 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/SingleKeyMatcher.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import org.mapsforge.core.Tag; + +class SingleKeyMatcher implements AttributeMatcher { + private final String mKey; + + SingleKeyMatcher(String key) { + mKey = key.intern(); + } + + @Override + public boolean isCoveredBy(AttributeMatcher attributeMatcher) { + Tag[] tags = { new Tag(mKey, null) }; + + return attributeMatcher == this || attributeMatcher.matches(tags); + } + + @Override + public boolean matches(Tag[] tags) { + for (Tag tag : tags) + if (mKey == tag.key) + return true; + + return false; + } +} diff --git a/src/org/mapsforge/android/rendertheme/SingleValueMatcher.java b/src/org/mapsforge/android/rendertheme/SingleValueMatcher.java new file mode 100644 index 00000000..5c07a498 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/SingleValueMatcher.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme; + +import org.mapsforge.core.Tag; + +class SingleValueMatcher implements AttributeMatcher { + private final String mValue; + + SingleValueMatcher(String value) { + mValue = value.intern(); + } + + @Override + public boolean isCoveredBy(AttributeMatcher attributeMatcher) { + Tag[] tags = { new Tag(null, mValue) }; + + return attributeMatcher == this || attributeMatcher.matches(tags); + } + + @Override + public boolean matches(Tag[] tags) { + for (Tag tag : tags) + if (mValue == tag.value) + return true; + + return false; + } +} diff --git a/src/org/mapsforge/android/rendertheme/osmarender/osmarender.xml b/src/org/mapsforge/android/rendertheme/osmarender/osmarender.xml new file mode 100644 index 00000000..ab359708 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/osmarender/osmarender.xml @@ -0,0 +1,689 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/org/mapsforge/android/rendertheme/renderTheme.xsd b/src/org/mapsforge/android/rendertheme/renderTheme.xsd new file mode 100644 index 00000000..03feede9 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderTheme.xsd @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/Area.java b/src/org/mapsforge/android/rendertheme/renderinstruction/Area.java new file mode 100644 index 00000000..2d908893 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/Area.java @@ -0,0 +1,162 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import java.io.IOException; + +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Cap; +import android.graphics.Paint.Style; +import android.graphics.Shader; + +/** + * Represents a closed polygon on the map. + */ +public final class Area implements RenderInstruction { + /** + * @param elementName + * the name of the XML element. + * @param attributes + * the attributes of the XML element. + * @param level + * the drawing level of this instruction. + * @return a new Area with the given rendering attributes. + * @throws IOException + * if an I/O error occurs while reading a resource. + */ + public static Area create(String elementName, Attributes attributes, int level) throws IOException { + String src = null; + int fill = Color.BLACK; + int stroke = Color.TRANSPARENT; + float strokeWidth = 0; + int fade = -1; + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("src".equals(name)) { + src = value; + } else if ("fill".equals(name)) { + fill = Color.parseColor(value); + } else if ("stroke".equals(name)) { + stroke = Color.parseColor(value); + } else if ("stroke-width".equals(name)) { + strokeWidth = Float.parseFloat(value); + } else if ("fade".equals(name)) { + fade = Integer.parseInt(value); + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(strokeWidth); + return new Area(src, fill, stroke, strokeWidth, fade, level); + } + + private static void validate(float strokeWidth) { + if (strokeWidth < 0) { + throw new IllegalArgumentException("stroke-width must not be negative: " + strokeWidth); + } + } + + /** + * + */ + public final int level; + /** + * + */ + public final Paint paintFill; + /** + * + */ + public final Paint paintOutline; + /** + * + */ + public final float strokeWidth; + /** + * + */ + public final int color; + /** + * + */ + public final int fade; + + private Area(String src, int fill, int stroke, float strokeWidth, int fade, int level) throws IOException { + super(); + + Shader shader = BitmapUtils.createBitmapShader(src); + + if (fill == Color.TRANSPARENT) { + paintFill = null; + } else { + paintFill = new Paint(Paint.ANTI_ALIAS_FLAG); + paintFill.setShader(shader); + paintFill.setStyle(Style.FILL); + paintFill.setColor(fill); + paintFill.setStrokeCap(Cap.ROUND); + } + + if (stroke == Color.TRANSPARENT) { + paintOutline = null; + } else { + paintOutline = new Paint(Paint.ANTI_ALIAS_FLAG); + paintOutline.setStyle(Style.STROKE); + paintOutline.setColor(stroke); + paintOutline.setStrokeCap(Cap.ROUND); + } + color = fill; + this.strokeWidth = strokeWidth; + this.fade = fade; + this.level = level; + } + + @Override + public void destroy() { + // do nothing + } + + @Override + public void renderNode(RenderCallback renderCallback, Tag[] tags) { + // do nothing + } + + @Override + public void renderWay(RenderCallback renderCallback, Tag[] tags) { + if (paintFill != null) { + renderCallback.renderArea(this); + } + } + + @Override + public void scaleStrokeWidth(float scaleFactor) { + if (paintOutline != null) { + paintOutline.setStrokeWidth(strokeWidth * scaleFactor); + } + } + + @Override + public void scaleTextSize(float scaleFactor) { + // do nothing + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/BitmapUtils.java b/src/org/mapsforge/android/rendertheme/renderinstruction/BitmapUtils.java new file mode 100644 index 00000000..bf1adade --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/BitmapUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapShader; +import android.graphics.Shader.TileMode; + +final class BitmapUtils { + private static final String PREFIX_FILE = "file:"; + private static final String PREFIX_JAR = "jar:"; + + private static InputStream createInputStream(String src) throws FileNotFoundException { + if (src.startsWith(PREFIX_JAR)) { + String name = src.substring(PREFIX_JAR.length()); + InputStream inputStream = Thread.currentThread().getClass().getResourceAsStream(name); + if (inputStream == null) { + throw new FileNotFoundException("resource not found: " + src); + } + return inputStream; + } else if (src.startsWith(PREFIX_FILE)) { + File file = new File(src.substring(PREFIX_FILE.length())); + if (!file.exists()) { + throw new IllegalArgumentException("file does not exist: " + src); + } else if (!file.isFile()) { + throw new IllegalArgumentException("not a file: " + src); + } else if (!file.canRead()) { + throw new IllegalArgumentException("cannot read file: " + src); + } + return new FileInputStream(file); + } + throw new IllegalArgumentException("invalid bitmap source: " + src); + } + + static Bitmap createBitmap(String src) throws IOException { + if (src == null || src.length() == 0) { + // no image source defined + return null; + } + + InputStream inputStream = createInputStream(src); + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + inputStream.close(); + return bitmap; + } + + static BitmapShader createBitmapShader(String src) throws IOException { + Bitmap bitmap = BitmapUtils.createBitmap(src); + if (bitmap == null) { + return null; + } + + return new BitmapShader(bitmap, TileMode.REPEAT, TileMode.REPEAT); + } + + private BitmapUtils() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/Caption.java b/src/org/mapsforge/android/rendertheme/renderinstruction/Caption.java new file mode 100644 index 00000000..e1e2c57c --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/Caption.java @@ -0,0 +1,143 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import java.util.Locale; + +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Typeface; + +/** + * Represents a text label on the map. + */ +public final class Caption implements RenderInstruction { + /** + * @param elementName + * the name of the XML element. + * @param attributes + * the attributes of the XML element. + * @return a new Caption with the given rendering attributes. + */ + public static Caption create(String elementName, Attributes attributes) { + String textKey = null; + float dy = 0; + FontFamily fontFamily = FontFamily.DEFAULT; + FontStyle fontStyle = FontStyle.NORMAL; + float fontSize = 0; + int fill = Color.BLACK; + int stroke = Color.BLACK; + float strokeWidth = 0; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("k".equals(name)) { + textKey = TextKey.getInstance(value); + } else if ("dy".equals(name)) { + dy = Float.parseFloat(value); + } else if ("font-family".equals(name)) { + fontFamily = FontFamily.valueOf(value.toUpperCase(Locale.ENGLISH)); + } else if ("font-style".equals(name)) { + fontStyle = FontStyle.valueOf(value.toUpperCase(Locale.ENGLISH)); + } else if ("font-size".equals(name)) { + fontSize = Float.parseFloat(value); + } else if ("fill".equals(name)) { + fill = Color.parseColor(value); + } else if ("stroke".equals(name)) { + stroke = Color.parseColor(value); + } else if ("stroke-width".equals(name)) { + strokeWidth = Float.parseFloat(value); + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(elementName, textKey, fontSize, strokeWidth); + Typeface typeface = Typeface.create(fontFamily.toTypeface(), fontStyle.toInt()); + return new Caption(textKey, dy, typeface, fontSize, fill, stroke, strokeWidth); + } + + private static void validate(String elementName, String textKey, float fontSize, float strokeWidth) { + if (textKey == null) { + throw new IllegalArgumentException("missing attribute k for element: " + elementName); + } else if (fontSize < 0) { + throw new IllegalArgumentException("font-size must not be negative: " + fontSize); + } else if (strokeWidth < 0) { + throw new IllegalArgumentException("stroke-width must not be negative: " + strokeWidth); + } + } + + private final float mDy; + private final float mFontSize; + private final Paint mPaint; + private final Paint mStroke; + private final String mTextKey; + + private Caption(String textKey, float dy, Typeface typeface, float fontSize, int fill, int stroke, float strokeWidth) { + super(); + + mTextKey = textKey; + mDy = dy; + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setTextAlign(Align.LEFT); + mPaint.setTypeface(typeface); + mPaint.setColor(fill); + + mStroke = new Paint(Paint.ANTI_ALIAS_FLAG); + mStroke.setStyle(Style.STROKE); + mStroke.setTextAlign(Align.LEFT); + mStroke.setTypeface(typeface); + mStroke.setColor(stroke); + mStroke.setStrokeWidth(strokeWidth); + + mFontSize = fontSize; + } + + @Override + public void destroy() { + // do nothing + } + + @Override + public void renderNode(RenderCallback renderCallback, Tag[] tags) { + renderCallback.renderPointOfInterestCaption(mTextKey, mDy, mPaint, mStroke); + } + + @Override + public void renderWay(RenderCallback renderCallback, Tag[] tags) { + renderCallback.renderAreaCaption(mTextKey, mDy, mPaint, mStroke); + } + + @Override + public void scaleStrokeWidth(float scaleFactor) { + // do nothing + } + + @Override + public void scaleTextSize(float scaleFactor) { + mPaint.setTextSize(mFontSize * scaleFactor); + mStroke.setTextSize(mFontSize * scaleFactor); + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/Circle.java b/src/org/mapsforge/android/rendertheme/renderinstruction/Circle.java new file mode 100644 index 00000000..808e050a --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/Circle.java @@ -0,0 +1,154 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; + +/** + * Represents a round area on the map. + */ +public final class Circle implements RenderInstruction { + /** + * @param elementName + * the name of the XML element. + * @param attributes + * the attributes of the XML element. + * @param level + * the drawing level of this instruction. + * @return a new Circle with the given rendering attributes. + */ + public static Circle create(String elementName, Attributes attributes, int level) { + Float radius = null; + boolean scaleRadius = false; + int fill = Color.TRANSPARENT; + int stroke = Color.TRANSPARENT; + float strokeWidth = 0; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("r".equals(name)) { + radius = Float.valueOf(Float.parseFloat(value)); + } else if ("scale-radius".equals(name)) { + scaleRadius = Boolean.parseBoolean(value); + } else if ("fill".equals(name)) { + fill = Color.parseColor(value); + } else if ("stroke".equals(name)) { + stroke = Color.parseColor(value); + } else if ("stroke-width".equals(name)) { + strokeWidth = Float.parseFloat(value); + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(elementName, radius, strokeWidth); + return new Circle(radius, scaleRadius, fill, stroke, strokeWidth, level); + } + + private static void validate(String elementName, Float radius, float strokeWidth) { + if (radius == null) { + throw new IllegalArgumentException("missing attribute r for element: " + elementName); + } else if (radius.floatValue() < 0) { + throw new IllegalArgumentException("radius must not be negative: " + radius); + } else if (strokeWidth < 0) { + throw new IllegalArgumentException("stroke-width must not be negative: " + strokeWidth); + } + } + + private final Paint mFill; + private final int mLevel; + private final Paint mOutline; + private final float mRadius; + private float mRenderRadius; + private final boolean mScaleRadius; + private final float mStrokeWidth; + + private Circle(Float radius, boolean scaleRadius, int fill, int stroke, float strokeWidth, int level) { + super(); + + mRadius = radius.floatValue(); + mScaleRadius = scaleRadius; + + if (fill == Color.TRANSPARENT) { + mFill = null; + } else { + mFill = new Paint(Paint.ANTI_ALIAS_FLAG); + mFill.setStyle(Style.FILL); + mFill.setColor(fill); + } + + if (stroke == Color.TRANSPARENT) { + mOutline = null; + } else { + mOutline = new Paint(Paint.ANTI_ALIAS_FLAG); + mOutline.setStyle(Style.STROKE); + mOutline.setColor(stroke); + } + + mStrokeWidth = strokeWidth; + mLevel = level; + + if (!mScaleRadius) { + mRenderRadius = mRadius; + if (mOutline != null) { + mOutline.setStrokeWidth(mStrokeWidth); + } + } + } + + @Override + public void destroy() { + // do nothing + } + + @Override + public void renderNode(RenderCallback renderCallback, Tag[] tags) { + if (mOutline != null) { + renderCallback.renderPointOfInterestCircle(mRenderRadius, mOutline, mLevel); + } + if (mFill != null) { + renderCallback.renderPointOfInterestCircle(mRenderRadius, mFill, mLevel); + } + } + + @Override + public void renderWay(RenderCallback renderCallback, Tag[] tags) { + // do nothing + } + + @Override + public void scaleStrokeWidth(float scaleFactor) { + if (mScaleRadius) { + mRenderRadius = mRadius * scaleFactor; + if (mOutline != null) { + mOutline.setStrokeWidth(mStrokeWidth * scaleFactor); + } + } + } + + @Override + public void scaleTextSize(float scaleFactor) { + // do nothing + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/FontFamily.java b/src/org/mapsforge/android/rendertheme/renderinstruction/FontFamily.java new file mode 100644 index 00000000..5c72f0eb --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/FontFamily.java @@ -0,0 +1,42 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import android.graphics.Typeface; + +enum FontFamily { + DEFAULT, DEFAULT_BOLD, MONOSPACE, SANS_SERIF, SERIF; + + /** + * @return the typeface object of this FontFamily. + * @see Typeface + */ + Typeface toTypeface() { + switch (this) { + case DEFAULT: + return Typeface.DEFAULT; + case DEFAULT_BOLD: + return Typeface.DEFAULT_BOLD; + case MONOSPACE: + return Typeface.MONOSPACE; + case SANS_SERIF: + return Typeface.SANS_SERIF; + case SERIF: + return Typeface.SERIF; + } + + throw new IllegalArgumentException("unknown enum value: " + this); + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/FontStyle.java b/src/org/mapsforge/android/rendertheme/renderinstruction/FontStyle.java new file mode 100644 index 00000000..ab6650ee --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/FontStyle.java @@ -0,0 +1,38 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +enum FontStyle { + BOLD, BOLD_ITALIC, ITALIC, NORMAL; + + /** + * @return the constant int value of this FontStyle. + * @see Typeface + */ + int toInt() { + switch (this) { + case BOLD: + return 1; + case BOLD_ITALIC: + return 3; + case ITALIC: + return 2; + case NORMAL: + return 0; + } + + throw new IllegalArgumentException("unknown enum value: " + this); + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/Line.java b/src/org/mapsforge/android/rendertheme/renderinstruction/Line.java new file mode 100644 index 00000000..5e34487c --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/Line.java @@ -0,0 +1,182 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import java.io.IOException; +import java.util.Locale; +import java.util.regex.Pattern; + +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Paint.Cap; +import android.graphics.Paint.Style; +import android.graphics.Shader; + +/** + * Represents a polyline on the map. + */ +public final class Line implements RenderInstruction { + private static final Pattern SPLIT_PATTERN = Pattern.compile(","); + + /** + * @param elementName + * the name of the XML element. + * @param attributes + * the attributes of the XML element. + * @param level + * the drawing level of this instruction. + * @return a new Line with the given rendering attributes. + * @throws IOException + * if an I/O error occurs while reading a resource. + */ + public static Line create(String elementName, Attributes attributes, int level) throws IOException { + String src = null; + int stroke = Color.BLACK; + float strokeWidth = 0; + float[] strokeDasharray = null; + Cap strokeLinecap = Cap.ROUND; + int outline = -1; + // int fade = -1; + boolean fixed = false; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("src".equals(name)) { + src = value; + } else if ("stroke".equals(name)) { + stroke = Color.parseColor(value); + } else if ("stroke-width".equals(name)) { + strokeWidth = Float.parseFloat(value); + } else if ("stroke-dasharray".equals(name)) { + strokeDasharray = parseFloatArray(value); + } else if ("stroke-linecap".equals(name)) { + strokeLinecap = Cap.valueOf(value.toUpperCase(Locale.ENGLISH)); + } else if ("outline".equals(name)) { + outline = Integer.parseInt(value); + } else if ("fade".equals(name)) { + // fade = Integer.parseInt(value); + } else if ("fixed".equals(name)) { + fixed = Boolean.parseBoolean(value); + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(strokeWidth); + return new Line(src, stroke, strokeWidth, strokeDasharray, strokeLinecap, level, outline, fixed); + } + + private static void validate(float strokeWidth) { + if (strokeWidth < 0) { + throw new IllegalArgumentException("stroke-width must not be negative: " + strokeWidth); + } + } + + static float[] parseFloatArray(String dashString) { + String[] dashEntries = SPLIT_PATTERN.split(dashString); + float[] dashIntervals = new float[dashEntries.length]; + for (int i = 0; i < dashEntries.length; ++i) { + dashIntervals[i] = Float.parseFloat(dashEntries[i]); + } + return dashIntervals; + } + + /** + * + */ + public final int level; + /** + * + */ + public final Paint paint; + /** + * + */ + public final float strokeWidth; + /** + * + */ + public final boolean round; + /** + * + */ + public final int color; + /** + * + */ + public final int outline; + + /** + * + */ + public final boolean fixed; + + private Line(String src, int stroke, float strokeWidth, float[] strokeDasharray, Cap strokeLinecap, int level, + int outline, boolean fixed) + throws IOException { + super(); + + Shader shader = BitmapUtils.createBitmapShader(src); + + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setShader(shader); + paint.setStyle(Style.STROKE); + paint.setColor(stroke); + if (strokeDasharray != null) { + paint.setPathEffect(new DashPathEffect(strokeDasharray, 0)); + } + paint.setStrokeCap(strokeLinecap); + round = strokeLinecap == Cap.ROUND; + this.color = stroke; + this.strokeWidth = strokeWidth; + this.level = level; + this.outline = outline; + this.fixed = fixed; + } + + @Override + public void destroy() { + // do nothing + } + + @Override + public void renderNode(RenderCallback renderCallback, Tag[] tags) { + // do nothing + } + + @Override + public void renderWay(RenderCallback renderCallback, Tag[] tags) { + // renderCallback.renderWay(mPaint, mLevel, mColor, mStrokeWidth, mRound, mOutline); + renderCallback.renderWay(this); + } + + @Override + public void scaleStrokeWidth(float scaleFactor) { + paint.setStrokeWidth(strokeWidth * scaleFactor); + } + + @Override + public void scaleTextSize(float scaleFactor) { + // do nothing + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/LineSymbol.java b/src/org/mapsforge/android/rendertheme/renderinstruction/LineSymbol.java new file mode 100644 index 00000000..75f74da3 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/LineSymbol.java @@ -0,0 +1,105 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import java.io.IOException; + +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +import android.graphics.Bitmap; + +/** + * Represents an icon along a polyline on the map. + */ +public final class LineSymbol implements RenderInstruction { + /** + * @param elementName + * the name of the XML element. + * @param attributes + * the attributes of the XML element. + * @return a new LineSymbol with the given rendering attributes. + * @throws IOException + * if an I/O error occurs while reading a resource. + */ + public static LineSymbol create(String elementName, Attributes attributes) throws IOException { + String src = null; + boolean alignCenter = false; + boolean repeat = false; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("src".equals(name)) { + src = value; + } else if ("align-center".equals(name)) { + alignCenter = Boolean.parseBoolean(value); + } else if ("repeat".equals(name)) { + repeat = Boolean.parseBoolean(value); + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(elementName, src); + return new LineSymbol(src, alignCenter, repeat); + } + + private static void validate(String elementName, String src) { + if (src == null) { + throw new IllegalArgumentException("missing attribute src for element: " + elementName); + } + } + + private final boolean mAlignCenter; + private final Bitmap mBitmap; + private final boolean mRepeat; + + private LineSymbol(String src, boolean alignCenter, boolean repeat) throws IOException { + super(); + + mBitmap = BitmapUtils.createBitmap(src); + mAlignCenter = alignCenter; + mRepeat = repeat; + } + + @Override + public void destroy() { + mBitmap.recycle(); + } + + @Override + public void renderNode(RenderCallback renderCallback, Tag[] tags) { + // do nothing + } + + @Override + public void renderWay(RenderCallback renderCallback, Tag[] tags) { + renderCallback.renderWaySymbol(mBitmap, mAlignCenter, mRepeat); + } + + @Override + public void scaleStrokeWidth(float scaleFactor) { + // do nothing + } + + @Override + public void scaleTextSize(float scaleFactor) { + // do nothing + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/PathText.java b/src/org/mapsforge/android/rendertheme/renderinstruction/PathText.java new file mode 100644 index 00000000..8b3f3c03 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/PathText.java @@ -0,0 +1,138 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import java.util.Locale; + +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Typeface; + +/** + * Represents a text along a polyline on the map. + */ +public final class PathText implements RenderInstruction { + /** + * @param elementName + * the name of the XML element. + * @param attributes + * the attributes of the XML element. + * @return a new PathText with the given rendering attributes. + */ + public static PathText create(String elementName, Attributes attributes) { + String textKey = null; + FontFamily fontFamily = FontFamily.DEFAULT; + FontStyle fontStyle = FontStyle.NORMAL; + float fontSize = 0; + int fill = Color.BLACK; + int stroke = Color.BLACK; + float strokeWidth = 0; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("k".equals(name)) { + textKey = TextKey.getInstance(value); + } else if ("font-family".equals(name)) { + fontFamily = FontFamily.valueOf(value.toUpperCase(Locale.ENGLISH)); + } else if ("font-style".equals(name)) { + fontStyle = FontStyle.valueOf(value.toUpperCase(Locale.ENGLISH)); + } else if ("font-size".equals(name)) { + fontSize = Float.parseFloat(value); + } else if ("fill".equals(name)) { + fill = Color.parseColor(value); + } else if ("stroke".equals(name)) { + stroke = Color.parseColor(value); + } else if ("stroke-width".equals(name)) { + strokeWidth = Float.parseFloat(value); + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(elementName, textKey, fontSize, strokeWidth); + Typeface typeface = Typeface.create(fontFamily.toTypeface(), fontStyle.toInt()); + return new PathText(textKey, typeface, fontSize, fill, stroke, strokeWidth); + } + + private static void validate(String elementName, String textKey, float fontSize, float strokeWidth) { + if (textKey == null) { + throw new IllegalArgumentException("missing attribute k for element: " + elementName); + } else if (fontSize < 0) { + throw new IllegalArgumentException("font-size must not be negative: " + fontSize); + } else if (strokeWidth < 0) { + throw new IllegalArgumentException("stroke-width must not be negative: " + strokeWidth); + } + } + + private final float mFontSize; + private final Paint mPaint; + private final Paint mStroke; + private final String mTextKey; + + private PathText(String textKey, Typeface typeface, float fontSize, int fill, int stroke, float strokeWidth) { + super(); + + mTextKey = textKey; + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setTextAlign(Align.CENTER); + mPaint.setTypeface(typeface); + mPaint.setColor(fill); + + mStroke = new Paint(Paint.ANTI_ALIAS_FLAG); + mStroke.setStyle(Style.STROKE); + mStroke.setTextAlign(Align.CENTER); + mStroke.setTypeface(typeface); + mStroke.setColor(stroke); + mStroke.setStrokeWidth(strokeWidth); + + mFontSize = fontSize; + } + + @Override + public void destroy() { + // do nothing + } + + @Override + public void renderNode(RenderCallback renderCallback, Tag[] tags) { + // do nothing + } + + @Override + public void renderWay(RenderCallback renderCallback, Tag[] tags) { + renderCallback.renderWayText(mTextKey, mPaint, mStroke); + } + + @Override + public void scaleStrokeWidth(float scaleFactor) { + // do nothing + } + + @Override + public void scaleTextSize(float scaleFactor) { + mPaint.setTextSize(mFontSize * scaleFactor); + mStroke.setTextSize(mFontSize * scaleFactor); + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/RenderInstruction.java b/src/org/mapsforge/android/rendertheme/renderinstruction/RenderInstruction.java new file mode 100644 index 00000000..08a307d4 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/RenderInstruction.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.core.Tag; + +/** + * A RenderInstruction is a basic graphical primitive to draw a map. + */ +public interface RenderInstruction { + /** + * Destroys this RenderInstruction and cleans up all its internal resources. + */ + void destroy(); + + /** + * @param renderCallback + * a reference to the receiver of all render callbacks. + * @param tags + * the tags of the node. + */ + void renderNode(RenderCallback renderCallback, Tag[] tags); + + /** + * @param renderCallback + * a reference to the receiver of all render callbacks. + * @param tags + * the tags of the way. + */ + void renderWay(RenderCallback renderCallback, Tag[] tags); + + /** + * Scales the stroke width of this RenderInstruction by the given factor. + * + * @param scaleFactor + * the factor by which the stroke width should be scaled. + */ + void scaleStrokeWidth(float scaleFactor); + + /** + * Scales the text size of this RenderInstruction by the given factor. + * + * @param scaleFactor + * the factor by which the text size should be scaled. + */ + void scaleTextSize(float scaleFactor); +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/Symbol.java b/src/org/mapsforge/android/rendertheme/renderinstruction/Symbol.java new file mode 100644 index 00000000..48a8feab --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/Symbol.java @@ -0,0 +1,95 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import java.io.IOException; + +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.core.Tag; +import org.xml.sax.Attributes; + +import android.graphics.Bitmap; + +/** + * Represents an icon on the map. + */ +public final class Symbol implements RenderInstruction { + /** + * @param elementName + * the name of the XML element. + * @param attributes + * the attributes of the XML element. + * @return a new Symbol with the given rendering attributes. + * @throws IOException + * if an I/O error occurs while reading a resource. + */ + public static Symbol create(String elementName, Attributes attributes) throws IOException { + String src = null; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("src".equals(name)) { + src = value; + } else { + RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); + } + } + + validate(elementName, src); + return new Symbol(src); + } + + private static void validate(String elementName, String src) { + if (src == null) { + throw new IllegalArgumentException("missing attribute src for element: " + elementName); + } + } + + private final Bitmap mBitmap; + + private Symbol(String src) throws IOException { + super(); + + mBitmap = BitmapUtils.createBitmap(src); + } + + @Override + public void destroy() { + mBitmap.recycle(); + } + + @Override + public void renderNode(RenderCallback renderCallback, Tag[] tags) { + renderCallback.renderPointOfInterestSymbol(mBitmap); + } + + @Override + public void renderWay(RenderCallback renderCallback, Tag[] tags) { + renderCallback.renderAreaSymbol(mBitmap); + } + + @Override + public void scaleStrokeWidth(float scaleFactor) { + // do nothing + } + + @Override + public void scaleTextSize(float scaleFactor) { + // do nothing + } +} diff --git a/src/org/mapsforge/android/rendertheme/renderinstruction/TextKey.java b/src/org/mapsforge/android/rendertheme/renderinstruction/TextKey.java new file mode 100644 index 00000000..8fffb116 --- /dev/null +++ b/src/org/mapsforge/android/rendertheme/renderinstruction/TextKey.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.rendertheme.renderinstruction; + +import org.mapsforge.core.Tag; + +final class TextKey { + static String getInstance(String key) { + if (Tag.TAG_KEY_ELE.equals(key)) { + return Tag.TAG_KEY_ELE; + } else if (Tag.TAG_KEY_HOUSE_NUMBER.equals(key)) { + return Tag.TAG_KEY_HOUSE_NUMBER; + } else if (Tag.TAG_KEY_NAME.equals(key)) { + return Tag.TAG_KEY_NAME; + } else if (Tag.TAG_KEY_REF.equals(key)) { + return Tag.TAG_KEY_REF; + } else { + throw new IllegalArgumentException("invalid key: " + key); + } + } +} diff --git a/src/org/mapsforge/android/swrenderer/CanvasRasterer.java b/src/org/mapsforge/android/swrenderer/CanvasRasterer.java new file mode 100644 index 00000000..7fa60d38 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/CanvasRasterer.java @@ -0,0 +1,254 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import java.util.List; + +import org.mapsforge.core.Tile; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Typeface; + +/** + * A CanvasRasterer uses a Canvas for drawing. + * + * @see Canvas + */ +class CanvasRasterer { + private static final Paint PAINT_BITMAP_FILTER = new Paint(Paint.FILTER_BITMAP_FLAG); + private static final Paint PAINT_TILE_COORDINATES = new Paint(Paint.ANTI_ALIAS_FLAG); + private static final Paint PAINT_TILE_COORDINATES_STROKE = new Paint(Paint.ANTI_ALIAS_FLAG); + private static final Paint PAINT_TILE_FRAME = new Paint(); + + private static final Paint PAINT_MARK = new Paint(); + static final int COLOR_MARK = Color.argb(30, 0, 255, 0); + + // private static final float[] TILE_FRAME = new float[] { 0, 0, 0, Tile.TILE_SIZE, 0, Tile.TILE_SIZE, + // Tile.TILE_SIZE, + // Tile.TILE_SIZE, Tile.TILE_SIZE, Tile.TILE_SIZE, Tile.TILE_SIZE, 0 }; + + private static void configurePaints() { + PAINT_TILE_COORDINATES.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + PAINT_TILE_COORDINATES.setTextSize(12); + + PAINT_TILE_COORDINATES_STROKE.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + PAINT_TILE_COORDINATES_STROKE.setStyle(Paint.Style.STROKE); + PAINT_TILE_COORDINATES_STROKE.setStrokeWidth(1); + PAINT_TILE_COORDINATES_STROKE.setTextSize(6); + PAINT_TILE_COORDINATES_STROKE.setColor(Color.WHITE); + PAINT_MARK.setColor(COLOR_MARK); + + } + + private final Canvas mCanvas; + private final Path mPath; + private final Matrix mSymbolMatrix; + + private float mScaleFactor; + + CanvasRasterer() { + mCanvas = new Canvas(); + mSymbolMatrix = new Matrix(); + mPath = new Path(); + mPath.setFillType(Path.FillType.EVEN_ODD); + mScaleFactor = 1; + configurePaints(); + + } + + private void drawTileCoordinate(String string, int offsetY) { + mCanvas.drawText(string, 20, offsetY, PAINT_TILE_COORDINATES); + } + + void drawNodes(List pointTextContainers) { + + for (int index = pointTextContainers.size() - 1; index >= 0; --index) { + PointTextContainer pointTextContainer = pointTextContainers.get(index); + + if (pointTextContainer.paintBack != null) { + + mCanvas.drawText(pointTextContainer.text, pointTextContainer.x * mScaleFactor, pointTextContainer.y + * mScaleFactor, pointTextContainer.paintBack); + } + + mCanvas.drawText(pointTextContainer.text, pointTextContainer.x * mScaleFactor, pointTextContainer.y + * mScaleFactor, pointTextContainer.paintFront); + } + } + + void drawSymbols(List symbolContainers) { + for (int index = symbolContainers.size() - 1; index >= 0; --index) { + SymbolContainer symbolContainer = symbolContainers.get(index); + + if (symbolContainer.alignCenter) { + int pivotX = symbolContainer.symbol.getWidth() >> 1; + int pivotY = symbolContainer.symbol.getHeight() >> 1; + mSymbolMatrix.setRotate(symbolContainer.rotation, pivotX, pivotY); + mSymbolMatrix.postTranslate(symbolContainer.x - pivotX, symbolContainer.y - pivotY); + } else { + mSymbolMatrix.setRotate(symbolContainer.rotation); + mSymbolMatrix.postTranslate(symbolContainer.x, symbolContainer.y); + } + mSymbolMatrix.postTranslate(mScaleFactor, mScaleFactor); + + // symbolMatrix.postScale(zoomFactor, zoomFactor); + mCanvas.drawBitmap(symbolContainer.symbol, mSymbolMatrix, PAINT_BITMAP_FILTER); + } + } + + void drawTileCoordinates(Tile tile, long time_load, long time_draw, long blub, long blah) { + + drawTileCoordinate(tile.tileX + " / " + tile.tileY + " / " + tile.zoomLevel + " " + mScaleFactor, 20); + + drawTileCoordinate("l:" + time_load, 40); + drawTileCoordinate("d:" + time_draw, 60); + drawTileCoordinate("+:" + blub, 80); + drawTileCoordinate("-:" + blah, 100); + + } + + void drawTileFrame() { + float size = (Tile.TILE_SIZE * mScaleFactor); + float[] frame = new float[] { 0, 0, 0, size - 1, 0, size - 1, size - 1, size - 1, size - 1, size - 1, size - 1, + 0 }; + mCanvas.drawLines(frame, PAINT_TILE_FRAME); + } + + void drawWayNames(float[] coords, List wayTextContainers) { + + for (int index = wayTextContainers.size() - 1; index >= 0; --index) { + WayTextContainer wayTextContainer = wayTextContainers.get(index); + mPath.rewind(); + + int first = wayTextContainer.first; + int last = wayTextContainer.last; + + // int len = wayTextContainer.wayDataContainer.length[0]; + // int pos = wayTextContainer.wayDataContainer.position[0]; + + // System.arraycopy(floats, pos, coords, 0, len); + + if (coords[first] < coords[last]) { + mPath.moveTo(coords[first], coords[first + 1]); + + for (int i = first + 2; i <= last; i += 2) { + mPath.lineTo(coords[i], coords[i + 1]); + } + } else { + mPath.moveTo(coords[last], coords[last + 1]); + + for (int i = last - 2; i >= first; i -= 2) { + mPath.lineTo(coords[i], coords[i + 1]); + } + } + mCanvas.drawTextOnPath(wayTextContainer.text, mPath, 0, 3, wayTextContainer.paint); + + // if (wayTextContainer.match) + // canvas.drawRect(wayTextContainer.x1, + // wayTextContainer.top, wayTextContainer.x2, + // wayTextContainer.bot, PAINT_MARK); + } + } + + void drawWays(float[] coords, LayerContainer[] drawWays) { + int levels = drawWays[0].mLevelActive.length; + + for (LayerContainer layerContainer : drawWays) { + if (!layerContainer.mActive) + continue; + + for (int level = 0; level < levels; level++) { + + if (!layerContainer.mLevelActive[level]) + continue; + + // mPath.rewind(); + + LevelContainer levelContainer = layerContainer.mLevels[level]; + + for (int way = levelContainer.mShapeContainers.size() - 1; way >= 0; way--) { + mPath.rewind(); + // switch (shapePaintContainer.shapeContainer.getShapeType()) { + // + // case WAY: + WayDataContainer wayDataContainer = (WayDataContainer) levelContainer.mShapeContainers.get(way); + // (WayDataContainer) shapePaintContainer.shapeContainer; + + // if (wayDataContainer.closed) { + for (int i = 0, n = wayDataContainer.length.length; i < n; i++) { + + int len = wayDataContainer.length[i]; + int pos = wayDataContainer.position[i]; + if (len > 2) { + mPath.moveTo(coords[pos], coords[pos + 1]); + + for (int j = pos + 2; j < len + pos; j += 2) + mPath.lineTo(coords[j], coords[j + 1]); + } + } + mCanvas.drawPath(mPath, levelContainer.mPaint[0]); + if (levelContainer.mPaint[1] != null) + mCanvas.drawPath(mPath, levelContainer.mPaint[1]); + + // }else { + // for (int i = 0, n = wayDataContainer.length.length; i < n; i++) { + // // levelContainer.mPaint[0].setStrokeJoin(Join.ROUND); + // + // int len = wayDataContainer.length[i]; + // int pos = wayDataContainer.position[i]; + // if (len > 2) { + // mCanvas.drawPoints(coords, pos, len, levelContainer.mPaint[0]); + // if (levelContainer.mPaint[1] != null) + // mCanvas.drawPoints(coords, pos, len, levelContainer.mPaint[1]); + // } + // + // } + // } + // break; + + // case CIRCLE: + // CircleContainer circleContainer = + // (CircleContainer) shapePaintContainer.shapeContainer; + // + // mPath.rewind(); + // + // mPath.addCircle(circleContainer.mX, circleContainer.mY, + // circleContainer.mRadius, Path.Direction.CCW); + // + // mCanvas.drawPath(mPath, shapePaintContainer.paint); + // break; + // } + + } + } + } + } + + void fill(int color) { + mCanvas.drawColor(color); + } + + void setCanvasBitmap(Bitmap bitmap, float scale) { + mCanvas.setBitmap(bitmap); + // add some extra pixels to avoid < 1px blank edges while scaling + mCanvas.clipRect(0, 0, Tile.TILE_SIZE * scale + 2, Tile.TILE_SIZE * scale + 2); + mScaleFactor = scale; + } +} diff --git a/src/org/mapsforge/android/swrenderer/CircleContainer.java b/src/org/mapsforge/android/swrenderer/CircleContainer.java new file mode 100644 index 00000000..759c1186 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/CircleContainer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + + +class CircleContainer implements ShapeContainer { + final float mRadius; + final float mX; + final float mY; + + CircleContainer(float x, float y, float radius) { + mX = x; + mY = y; + mRadius = radius; + } + + @Override + public ShapeType getShapeType() { + return ShapeType.CIRCLE; + } +} diff --git a/src/org/mapsforge/android/swrenderer/DatabaseRenderer.java b/src/org/mapsforge/android/swrenderer/DatabaseRenderer.java new file mode 100644 index 00000000..40575434 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/DatabaseRenderer.java @@ -0,0 +1,581 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; + +import org.mapsforge.android.MapView; +import org.mapsforge.android.mapgenerator.JobTheme; +import org.mapsforge.android.mapgenerator.MapGenerator; +import org.mapsforge.android.mapgenerator.MapGeneratorJob; +import org.mapsforge.android.rendertheme.RenderCallback; +import org.mapsforge.android.rendertheme.RenderTheme; +import org.mapsforge.android.rendertheme.RenderThemeHandler; +import org.mapsforge.android.rendertheme.renderinstruction.Area; +import org.mapsforge.android.rendertheme.renderinstruction.Line; +import org.mapsforge.core.GeoPoint; +import org.mapsforge.core.Tag; +import org.mapsforge.core.Tile; +import org.mapsforge.mapdatabase.IMapDatabase; +import org.mapsforge.mapdatabase.IMapDatabaseCallback; +import org.mapsforge.mapdatabase.MapFileInfo; +import org.mapsforge.mapdatabase.mapfile.MapDatabase; +import org.xml.sax.SAXException; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.FloatMath; +import android.util.Log; + +/** + * A DatabaseRenderer renders map tiles by reading from a {@link MapDatabase}. + */ +public class DatabaseRenderer implements MapGenerator, RenderCallback, IMapDatabaseCallback { + private static String TAG = DatabaseRenderer.class.getName(); + private static final Byte DEFAULT_START_ZOOM_LEVEL = Byte.valueOf((byte) 12); + private static final byte LAYERS = 11; + private static final Paint PAINT_WATER_TILE_HIGHTLIGHT = new Paint(Paint.ANTI_ALIAS_FLAG); + private static final double STROKE_INCREASE = 1.5; + private static final byte STROKE_MIN_ZOOM_LEVEL = 12; + + private static final byte ZOOM_MAX = 22; + + // private static MapRenderer mMapRenderer; + + private static RenderTheme getRenderTheme(JobTheme jobTheme) { + InputStream inputStream = null; + try { + inputStream = jobTheme.getRenderThemeAsStream(); + return RenderThemeHandler.getRenderTheme(inputStream); + } catch (ParserConfigurationException e) { + Log.e(TAG, e.getMessage()); + } catch (SAXException e) { + Log.e(TAG, e.getMessage()); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + } + } + return null; + } + + private static byte getValidLayer(byte layer) { + if (layer < 0) { + return 0; + } else if (layer >= LAYERS) { + return LAYERS - 1; + } else { + return layer; + } + } + + private final CanvasRasterer mCanvasRasterer; + + private LayerContainer mDrawingLayer; + private final LabelPlacement mLabelPlacement; + private IMapDatabase mMapDatabase; + private List mNodes; + private float mPoiX; + private float mPoiY; + private JobTheme mPreviousJobTheme; + private float mPreviousTextScale; + private byte mPreviousZoomLevel; + private static RenderTheme renderTheme; + + private final List mWayNames; + private final LayerContainer[] mWays; + private final List mWaySymbols; + private final List mPointSymbols; + private final List mAreaLabels; + + // private float mLat1, mLat2, mLon1, mLon2; + // private float mTileWidth, mTileHeight; + private float mScale; + + // private float[] mCoordinates; + private WayDataContainer mWayDataContainer; + private final Bitmap mTileBitmap; + + private static float PI180 = (float) (Math.PI / 180) / 1000000.0f; + private static float PIx4 = (float) Math.PI * 4; + + private Tile mCurrentTile; + private static long mCurrentTileY; + private static long mCurrentTileX; + private static long mCurrentTileZoom; + + private float[] mCoords = null; + + // private long _renderTime; + private int _nodes, _nodesDropped; + + /** + * Constructs a new DatabaseRenderer. + */ + public DatabaseRenderer() { + mCanvasRasterer = new CanvasRasterer(); + mLabelPlacement = new LabelPlacement(); + + mWays = new LayerContainer[LAYERS]; + mWayNames = new ArrayList(64); + mNodes = new ArrayList(64); + mAreaLabels = new ArrayList(64); + mWaySymbols = new ArrayList(64); + mPointSymbols = new ArrayList(64); + + PAINT_WATER_TILE_HIGHTLIGHT.setStyle(Paint.Style.FILL); + PAINT_WATER_TILE_HIGHTLIGHT.setColor(Color.CYAN); + // mCoordinates = new float[1024]; + + mTileBitmap = Bitmap.createBitmap(Tile.TILE_SIZE * 2, Tile.TILE_SIZE * 2, Bitmap.Config.RGB_565); + + } + + @Override + public void cleanup() { + mTileBitmap.recycle(); + if (DatabaseRenderer.renderTheme != null) { + DatabaseRenderer.renderTheme.destroy(); + } + } + + @Override + public boolean executeJob(MapGeneratorJob mapGeneratorJob) { + long time_load = System.currentTimeMillis(); + _nodes = 0; + _nodesDropped = 0; + // _renderTime = 0; + + mCurrentTile = mapGeneratorJob.tile; + mCurrentTileZoom = ((long) Tile.TILE_SIZE << mCurrentTile.zoomLevel); + mCurrentTileX = mCurrentTile.pixelX; + mCurrentTileY = mCurrentTile.pixelY; + + // mLon1 = (float) MercatorProjection.pixelXToLongitude(mCurrentTileX, mCurrentTile.zoomLevel) * 1000000; + // mLat1 = (float) MercatorProjection.pixelYToLatitude(mCurrentTileY, mCurrentTile.zoomLevel) * 1000000; + // mLon2 = (float) MercatorProjection.pixelXToLongitude(mCurrentTileX + Tile.TILE_SIZE, mCurrentTile.zoomLevel) + // * 1000000; + // mLat2 = (float) MercatorProjection.pixelYToLatitude(mCurrentTileY + Tile.TILE_SIZE, mCurrentTile.zoomLevel) * + // 1000000; + // + // mTileWidth = mLon2 - mLon1; + // mTileHeight = mLat1 - mLat2; + mScale = mapGeneratorJob.getScale(); + + JobTheme jobTheme = mapGeneratorJob.jobParameters.jobTheme; + if (!jobTheme.equals(mPreviousJobTheme)) { + if (DatabaseRenderer.renderTheme == null) + DatabaseRenderer.renderTheme = getRenderTheme(jobTheme); + if (DatabaseRenderer.renderTheme == null) { + mPreviousJobTheme = null; + return false; + } + createWayLists(); + mPreviousJobTheme = jobTheme; + mPreviousZoomLevel = Byte.MIN_VALUE; + } + + byte zoomLevel = mCurrentTile.zoomLevel; + if (zoomLevel != mPreviousZoomLevel) { + setScaleStrokeWidth(zoomLevel); + mPreviousZoomLevel = zoomLevel; + } + + float textScale = mapGeneratorJob.jobParameters.textScale; + if (textScale != mPreviousTextScale) { + DatabaseRenderer.renderTheme.scaleTextSize(textScale); + mPreviousTextScale = textScale; + } + + if (mMapDatabase != null) { + mMapDatabase.executeQuery(mCurrentTile, this); + } + else { + return false; + } + time_load = System.currentTimeMillis() - time_load; + + mNodes = mLabelPlacement.placeLabels(mNodes, mPointSymbols, mAreaLabels, mCurrentTile); + + long time_draw = System.currentTimeMillis(); + + // FIXME mCoords = mMapDatabase.getCoordinates(); + + mCanvasRasterer.setCanvasBitmap(mTileBitmap, mScale); + mCanvasRasterer.fill(DatabaseRenderer.renderTheme.getMapBackground()); + mCanvasRasterer.drawWays(mCoords, mWays); + mCanvasRasterer.drawSymbols(mWaySymbols); + mCanvasRasterer.drawSymbols(mPointSymbols); + mCanvasRasterer.drawWayNames(mCoords, mWayNames); + mCanvasRasterer.drawNodes(mNodes); + mCanvasRasterer.drawNodes(mAreaLabels); + time_draw = System.currentTimeMillis() - time_draw; + + if (mapGeneratorJob.debugSettings.mDrawTileFrames) { + mCanvasRasterer.drawTileFrame(); + } + + if (mapGeneratorJob.debugSettings.mDrawTileCoordinates) { + mCanvasRasterer.drawTileCoordinates(mCurrentTile, time_load, time_draw, _nodes, _nodesDropped); + } + + clearLists(); + + mapGeneratorJob.setBitmap(mTileBitmap); + + return true; + } + + @Override + public GeoPoint getStartPoint() { + if (mMapDatabase != null && mMapDatabase.hasOpenFile()) { + MapFileInfo mapFileInfo = mMapDatabase.getMapFileInfo(); + if (mapFileInfo.startPosition != null) { + return mapFileInfo.startPosition; + } else if (mapFileInfo.mapCenter != null) { + return mapFileInfo.mapCenter; + } + } + + return null; + } + + @Override + public Byte getStartZoomLevel() { + if (mMapDatabase != null && mMapDatabase.hasOpenFile()) { + MapFileInfo mapFileInfo = mMapDatabase.getMapFileInfo(); + if (mapFileInfo.startZoomLevel != null) { + return mapFileInfo.startZoomLevel; + } + } + + return DEFAULT_START_ZOOM_LEVEL; + } + + @Override + public byte getZoomLevelMax() { + return ZOOM_MAX; + } + + @Override + public void renderAreaCaption(String textKey, float verticalOffset, Paint paint, Paint stroke) { + // mapDatabase.readTag(caption); + // if (caption.value != null) { + // float[] centerPosition = GeometryUtils + // .calculateCenterOfBoundingBox(coordinates[0]); + // areaLabels.add(new PointTextContainer(caption.value, + // centerPosition[0], + // centerPosition[1], + // paint, stroke)); + // } + } + + @Override + public void renderAreaSymbol(Bitmap symbol) { + // float[] centerPosition = GeometryUtils + // .calculateCenterOfBoundingBox(coordinates[0]); + // pointSymbols.add(new SymbolContainer(symbol, centerPosition[0] + // - (symbol.getWidth() >> 1), centerPosition[1] + // - (symbol.getHeight() >> 1))); + } + + @Override + public void renderPointOfInterest(byte layer, int latitude, int longitude, Tag[] tags) { + mDrawingLayer = mWays[getValidLayer(layer)]; + mPoiX = scaleLongitude(longitude); + mPoiY = scaleLatitude(latitude); + DatabaseRenderer.renderTheme.matchNode(this, tags, mCurrentTile.zoomLevel); + } + + @Override + public void renderPointOfInterestCaption(String textKey, float verticalOffset, Paint paint, Paint stroke) { + // mapDatabase.readTag(caption); + // if (caption.value != null) { + // nodes.add(new PointTextContainer(caption.value, poiX, poiY + verticalOffset, paint, stroke)); + // } + } + + @Override + public void renderPointOfInterestCircle(float radius, Paint outline, int level) { + + mDrawingLayer.add(level, new CircleContainer(mPoiX, mPoiY, radius), outline); + } + + @Override + public void renderPointOfInterestSymbol(Bitmap symbol) { + mPointSymbols.add(new SymbolContainer(symbol, mPoiX - (symbol.getWidth() >> 1), mPoiY + - (symbol.getHeight() >> 1))); + } + + @Override + public void renderWaterBackground() { + // if (mCoords == null) + // mCoords = mMapDatabase.getCoordinates(); + + // float[] coords = mCoords; + // + // mDrawingLayer = mWays[5]; + // + // int len = wayData.length[0]; + // int pos = wayData.position[0]; + // + // for (int j = pos, m = pos + len; j < m; j += 2) { + // coords[j] = coords[j] * mScale; + // coords[j + 1] = coords[j + 1] * mScale; + // } + // + // mWayDataContainer = wayData; + // + // Log.i("mapsforge", "render water"); + // + // DatabaseRenderer.renderTheme.matchWay(this, tags, mCurrentTile.zoomLevel, true); + } + + // private boolean mPrevClosed = false; + // private byte mPrevLayer = 0; + + @Override + public void renderWay(byte layer, Tag[] tags, float[] wayNodes, int[] wayLengths, boolean changed) { + // if (mCoords == null) + // mCoords = mMapDatabase.getCoordinates(); + + // float[] coords = mCoords; + // + // boolean closed = false; + // boolean added = false; + // + // // coordinatesLength = wayData.length.length; + // if (mCurrentTile.zoomLevel < 6) { + // long x = mCurrentTileX; + // long y = mCurrentTileY; + // long z = mCurrentTileZoom; + // float s = mScale; + // + // added = true; + // + // for (int i = wayData.length.length - 1; i >= 0; i--) { + // int len = wayData.length[i]; + // int pos = wayData.position[i]; + // + // if (i == 0) + // closed = (coords[pos] == coords[(pos + len) - 2] && + // coords[pos + 1] == coords[(pos + len) - 1]); + // + // for (int j = pos, m = pos + len; j < m; j += 2) { + // + // coords[j] = (float) (((coords[j] / 1000000.0 + 180) / 360 * z) - x) * s; + // + // double sinLat = Math.sin(coords[j + 1] * PI180); + // coords[j + 1] = (float) ((0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / PIx4) * z - y) * s; + // } + // } + // } else { + // // use linear approximation on high zoom levels. + // float ssize = Tile.TILE_SIZE * mScale; + // float sw = ssize / mTileWidth; + // float sh = ssize / mTileHeight; + // int j, o; + // float x, y; + // + // int min = 1; + // if (mCurrentTile.zoomLevel < 14) + // min = 3; + // else if (mCurrentTile.zoomLevel < 9) + // min = 5; + // + // for (int i = wayData.length.length - 1; i >= 0; i--) { + // + // int len = wayData.length[i]; + // int pos = wayData.position[i]; + // _nodes += len / 2; + // + // if (i == 0) { + // closed = (coords[pos] == coords[(pos + len) - 2] && + // coords[pos + 1] == coords[(pos + len) - 1]); + // } + // + // coords[pos] = (coords[pos] - mLon1) * sw; + // coords[pos + 1] = ssize - (coords[pos + 1] - mLat2) * sh; + // + // j = o = pos + 2; + // + // // drop intermediate nodes with less than 'min' distance. + // for (int m = pos + len - 2; j < m; j += 2) { + // x = (coords[j] - mLon1) * sw; + // y = ssize - (coords[j + 1] - mLat2) * sh; + // + // if (x > coords[o - 2] + min || x < coords[o - 2] - min || + // y > coords[o - 1] + min || y < coords[o - 1] - min) { + // + // coords[o++] = x; + // coords[o++] = y; + // } else + // _nodesDropped++; + // } + // coords[o] = (coords[j] - mLon1) * sw; + // coords[o + 1] = ssize - (coords[j + 1] - mLat2) * sh; + // o += 2; + // + // wayData.length[i] = o - pos; + // + // if (!closed || (o - pos) > 4) + // added = true; + // else + // wayData.length[i] = 0; + // } + // } + // + // if (!added && !changed) + // return; + // + // mWayDataContainer = wayData; + // + // mDrawingLayer = mWays[getValidLayer(layer)]; + // + // if (changed || (closed != mPrevClosed) || (layer != mPrevLayer)) { + // mCurLevelContainer1 = null; + // mCurLevelContainer2 = null; + // DatabaseRenderer.renderTheme.matchWay(this, tags, mCurrentTile.zoomLevel, closed); + // } else { + // if (mCurLevelContainer1 != null) + // mCurLevelContainer1.add(mWayDataContainer); + // if (mCurLevelContainer2 != null) + // mCurLevelContainer2.add(mWayDataContainer); + // } + // mPrevClosed = closed; + // mPrevLayer = layer; + } + + private List mCurLevelContainer1; + private List mCurLevelContainer2; + + @Override + public void renderWay(Line line) { + List c = mDrawingLayer.add(line.level, mWayDataContainer, line.paint); + + if (mCurLevelContainer1 == null) + mCurLevelContainer1 = c; + else if (mCurLevelContainer2 == null) + mCurLevelContainer2 = c; + } + + @Override + public void renderArea(Area area) { + if (area.paintFill != null) + mCurLevelContainer1 = mDrawingLayer.add(area.level, mWayDataContainer, area.paintFill); + if (area.paintOutline != null) + mCurLevelContainer1 = mDrawingLayer.add(area.level, mWayDataContainer, area.paintOutline); + } + + @Override + public void renderWaySymbol(Bitmap symbolBitmap, boolean alignCenter, boolean repeatSymbol) { + // WayDecorator.renderSymbol(symbolBitmap, alignCenter, repeatSymbol, + // coordinates, + // waySymbols); + } + + @Override + public void renderWayText(String textKey, Paint paint, Paint outline) { + // if (mWayDataContainer.textPos[0] >= 0) + // WayDecorator.renderText(this, paint, outline, mCoords, mWayDataContainer, mWayNames); + } + + String getWayName() { + return mMapDatabase.readString(mWayDataContainer.textPos[0]); + } + + @Override + public boolean requiresInternetConnection() { + return false; + } + + @Override + public void setMapDatabase(IMapDatabase mapDatabase) { + mMapDatabase = mapDatabase; + } + + private void clearLists() { + for (int i = LAYERS - 1; i >= 0; --i) { + mWays[i].clear(); + } + + mAreaLabels.clear(); + mNodes.clear(); + mPointSymbols.clear(); + mWayNames.clear(); + mWaySymbols.clear(); + } + + private void createWayLists() { + int levels = DatabaseRenderer.renderTheme.getLevels(); + for (byte i = LAYERS - 1; i >= 0; --i) { + mWays[i] = new LayerContainer(levels); + } + } + + /** + * Converts a latitude value into an Y coordinate on the current tile. + * + * @param latitude + * the latitude value. + * @return the Y coordinate on the current tile. + */ + private static float scaleLatitude(float latitude) { + double sinLatitude = FloatMath.sin(latitude * PI180); + + return (float) (0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / PIx4) * mCurrentTileZoom + - mCurrentTileY; + } + + /** + * Converts a longitude value into an X coordinate on the current tile. + * + * @param longitude + * the longitude value. + * @return the X coordinate on the current tile. + */ + + private static float scaleLongitude(float longitude) { + return (float) ((longitude / 1000000.0 + 180) / 360 * mCurrentTileZoom) - mCurrentTileX; + } + + /** + * Sets the scale stroke factor for the given zoom level. + * + * @param zoomLevel + * the zoom level for which the scale stroke factor should be set. + */ + private static void setScaleStrokeWidth(byte zoomLevel) { + int zoomLevelDiff = Math.max(zoomLevel - STROKE_MIN_ZOOM_LEVEL, 0); + DatabaseRenderer.renderTheme.scaleStrokeWidth((float) Math.pow(STROKE_INCREASE, zoomLevelDiff)); + } + + @Override + public MapRenderer getMapRenderer(MapView mapView) { + return new MapRenderer(mapView); + } +} diff --git a/src/org/mapsforge/android/swrenderer/DependencyCache.java b/src/org/mapsforge/android/swrenderer/DependencyCache.java new file mode 100644 index 00000000..9ba3bae5 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/DependencyCache.java @@ -0,0 +1,985 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.mapsforge.core.Tile; + +import android.graphics.Bitmap; +import android.graphics.Paint; +import android.graphics.Rect; + +/** + * This class process the methods for the Dependency Cache. It's connected with the LabelPlacement class. The main goal + * is, to remove double labels and symbols that are already rendered, from the actual tile. Labels and symbols that, + * would be rendered on an already drawn Tile, will be deleted too. + */ +class DependencyCache { + /** + * The class holds the data for a symbol with dependencies on other tiles. + * + * @param + * only two types are reasonable. The DependencySymbol or DependencyText class. + */ + private static class Dependency { + ImmutablePoint point; + final Type value; + + Dependency(Type value, ImmutablePoint point) { + this.value = value; + this.point = point; + } + } + + /** + * This class holds all the information off the possible dependencies on a tile. + */ + private static class DependencyOnTile { + boolean drawn; + List> labels; + List> symbols; + + /** + * Initialize label, symbol and drawn. + */ + DependencyOnTile() { + this.labels = null; + this.symbols = null; + this.drawn = false; + } + + /** + * @param toAdd + * a dependency Symbol + */ + void addSymbol(Dependency toAdd) { + if (this.symbols == null) { + this.symbols = new ArrayList>(); + } + this.symbols.add(toAdd); + } + + /** + * @param toAdd + * a Dependency Text + */ + void addText(Dependency toAdd) { + if (this.labels == null) { + this.labels = new ArrayList>(); + } + this.labels.add(toAdd); + } + } + + /** + * The class holds the data for a symbol with dependencies on other tiles. + */ + private static class DependencySymbol { + private final List tiles; + Bitmap symbol; + + /** + * Creates a symbol dependency element for the dependency cache. + * + * @param symbol + * reference on the dependency symbol. + * @param tile + * dependency tile. + */ + DependencySymbol(Bitmap symbol, Tile tile) { + this.symbol = symbol; + this.tiles = new LinkedList(); + this.tiles.add(tile); + } + + /** + * Adds an additional tile, which has an dependency with this symbol. + * + * @param tile + * additional tile. + */ + void addTile(Tile tile) { + this.tiles.add(tile); + } + } + + /** + * The class holds the data for a label with dependencies on other tiles. + */ + private static class DependencyText { + final Rect boundary; + final Paint paintBack; + final Paint paintFront; + final String text; + List tiles; + + /** + * Creates a text dependency in the dependency cache. + * + * @param paintFront + * paint element from the front. + * @param paintBack + * paint element form the background of the text. + * @param text + * the text of the element. + * @param boundary + * the fixed boundary with width and height. + * @param tile + * all tile in where the element has an influence. + */ + DependencyText(Paint paintFront, Paint paintBack, String text, Rect boundary, Tile tile) { + this.paintFront = paintFront; + this.paintBack = paintBack; + this.text = text; + this.tiles = new LinkedList(); + this.tiles.add(tile); + this.boundary = boundary; + } + + void addTile(Tile tile) { + this.tiles.add(tile); + } + } + + private DependencyOnTile currentDependencyOnTile; + private Tile currentTile; + + /** + * Hash table, that connects the Tiles with their entries in the dependency cache. + */ + final Map dependencyTable; + Dependency depLabel; + Rect rect1; + Rect rect2; + SymbolContainer smb; + DependencyOnTile tmp; + + /** + * Constructor for this class, that creates a hashtable for the dependencies. + */ + DependencyCache() { + this.dependencyTable = new Hashtable(60); + } + + private void addLabelsFromDependencyOnTile(List labels) { + for (int i = 0; i < this.currentDependencyOnTile.labels.size(); i++) { + this.depLabel = this.currentDependencyOnTile.labels.get(i); + if (this.depLabel.value.paintBack != null) { + labels.add(new PointTextContainer(this.depLabel.value.text, this.depLabel.point.pointX, + this.depLabel.point.pointY, this.depLabel.value.paintFront, this.depLabel.value.paintBack)); + } else { + labels.add(new PointTextContainer(this.depLabel.value.text, this.depLabel.point.pointX, + this.depLabel.point.pointY, this.depLabel.value.paintFront)); + } + } + } + + private void addSymbolsFromDependencyOnTile(List symbols) { + for (Dependency depSmb : this.currentDependencyOnTile.symbols) { + symbols.add(new SymbolContainer(depSmb.value.symbol, depSmb.point.pointX, depSmb.point.pointY)); + } + } + + /** + * Fills the dependency entry from the tile and the neighbor tiles with the dependency information, that are + * necessary for drawing. To do that every label and symbol that will be drawn, will be checked if it produces + * dependencies with other tiles. + * + * @param pTC + * list of the labels + */ + private void fillDependencyLabels(List pTC) { + Tile left = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile right = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile up = new Tile(this.currentTile.tileX, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile down = new Tile(this.currentTile.tileX, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + + Tile leftup = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile leftdown = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + Tile rightup = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile rightdown = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + + PointTextContainer label; + DependencyOnTile linkedDep; + DependencyText toAdd; + + for (int i = 0; i < pTC.size(); i++) { + + label = pTC.get(i); + + toAdd = null; + + // up + if ((label.y - label.boundary.height() < 0.0f) && (!this.dependencyTable.get(up).drawn)) { + linkedDep = this.dependencyTable.get(up); + + toAdd = new DependencyText(label.paintFront, label.paintBack, label.text, label.boundary, + this.currentTile); + + this.currentDependencyOnTile.addText(new Dependency(toAdd, new ImmutablePoint(label.x, + label.y))); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x, label.y + + Tile.TILE_SIZE))); + + toAdd.addTile(up); + + if ((label.x < 0.0f) && (!this.dependencyTable.get(leftup).drawn)) { + linkedDep = this.dependencyTable.get(leftup); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint( + label.x + Tile.TILE_SIZE, label.y + Tile.TILE_SIZE))); + + toAdd.addTile(leftup); + } + + if ((label.x + label.boundary.width() > Tile.TILE_SIZE) && (!this.dependencyTable.get(rightup).drawn)) { + linkedDep = this.dependencyTable.get(rightup); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint( + label.x - Tile.TILE_SIZE, label.y + Tile.TILE_SIZE))); + + toAdd.addTile(rightup); + } + } + + // down + if ((label.y > Tile.TILE_SIZE) && (!this.dependencyTable.get(down).drawn)) { + + linkedDep = this.dependencyTable.get(down); + + if (toAdd == null) { + toAdd = new DependencyText(label.paintFront, label.paintBack, label.text, label.boundary, + this.currentTile); + + this.currentDependencyOnTile.addText(new Dependency(toAdd, new ImmutablePoint( + label.x, label.y))); + + } + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x, label.y + - Tile.TILE_SIZE))); + + toAdd.addTile(down); + + if ((label.x < 0.0f) && (!this.dependencyTable.get(leftdown).drawn)) { + linkedDep = this.dependencyTable.get(leftdown); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint( + label.x + Tile.TILE_SIZE, label.y - Tile.TILE_SIZE))); + + toAdd.addTile(leftdown); + } + + if ((label.x + label.boundary.width() > Tile.TILE_SIZE) && (!this.dependencyTable.get(rightdown).drawn)) { + + linkedDep = this.dependencyTable.get(rightdown); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint( + label.x - Tile.TILE_SIZE, label.y - Tile.TILE_SIZE))); + + toAdd.addTile(rightdown); + } + } + // left + + if ((label.x < 0.0f) && (!this.dependencyTable.get(left).drawn)) { + linkedDep = this.dependencyTable.get(left); + + if (toAdd == null) { + toAdd = new DependencyText(label.paintFront, label.paintBack, label.text, label.boundary, + this.currentTile); + + this.currentDependencyOnTile.addText(new Dependency(toAdd, new ImmutablePoint( + label.x, label.y))); + } + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x + Tile.TILE_SIZE, + label.y))); + + toAdd.addTile(left); + } + // right + if ((label.x + label.boundary.width() > Tile.TILE_SIZE) && (!this.dependencyTable.get(right).drawn)) { + linkedDep = this.dependencyTable.get(right); + + if (toAdd == null) { + toAdd = new DependencyText(label.paintFront, label.paintBack, label.text, label.boundary, + this.currentTile); + + this.currentDependencyOnTile.addText(new Dependency(toAdd, new ImmutablePoint( + label.x, label.y))); + } + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x - Tile.TILE_SIZE, + label.y))); + + toAdd.addTile(right); + } + + // check symbols + + if ((label.symbol != null) && (toAdd == null)) { + + if ((label.symbol.y <= 0.0f) && (!this.dependencyTable.get(up).drawn)) { + linkedDep = this.dependencyTable.get(up); + + toAdd = new DependencyText(label.paintFront, label.paintBack, label.text, label.boundary, + this.currentTile); + + this.currentDependencyOnTile.addText(new Dependency(toAdd, new ImmutablePoint( + label.x, label.y))); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x, label.y + + Tile.TILE_SIZE))); + + toAdd.addTile(up); + + if ((label.symbol.x < 0.0f) && (!this.dependencyTable.get(leftup).drawn)) { + linkedDep = this.dependencyTable.get(leftup); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x + + Tile.TILE_SIZE, label.y + Tile.TILE_SIZE))); + + toAdd.addTile(leftup); + } + + if ((label.symbol.x + label.symbol.symbol.getWidth() > Tile.TILE_SIZE) + && (!this.dependencyTable.get(rightup).drawn)) { + linkedDep = this.dependencyTable.get(rightup); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x + - Tile.TILE_SIZE, label.y + Tile.TILE_SIZE))); + + toAdd.addTile(rightup); + } + } + + if ((label.symbol.y + label.symbol.symbol.getHeight() >= Tile.TILE_SIZE) + && (!this.dependencyTable.get(down).drawn)) { + + linkedDep = this.dependencyTable.get(down); + + if (toAdd == null) { + toAdd = new DependencyText(label.paintFront, label.paintBack, label.text, label.boundary, + this.currentTile); + + this.currentDependencyOnTile.addText(new Dependency(toAdd, new ImmutablePoint( + label.x, label.y))); + } + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x, label.y + + Tile.TILE_SIZE))); + + toAdd.addTile(up); + + if ((label.symbol.x < 0.0f) && (!this.dependencyTable.get(leftdown).drawn)) { + linkedDep = this.dependencyTable.get(leftdown); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x + + Tile.TILE_SIZE, label.y - Tile.TILE_SIZE))); + + toAdd.addTile(leftdown); + } + + if ((label.symbol.x + label.symbol.symbol.getWidth() > Tile.TILE_SIZE) + && (!this.dependencyTable.get(rightdown).drawn)) { + + linkedDep = this.dependencyTable.get(rightdown); + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint(label.x + - Tile.TILE_SIZE, label.y - Tile.TILE_SIZE))); + + toAdd.addTile(rightdown); + } + } + + if ((label.symbol.x <= 0.0f) && (!this.dependencyTable.get(left).drawn)) { + linkedDep = this.dependencyTable.get(left); + + if (toAdd == null) { + toAdd = new DependencyText(label.paintFront, label.paintBack, label.text, label.boundary, + this.currentTile); + + this.currentDependencyOnTile.addText(new Dependency(toAdd, new ImmutablePoint( + label.x, label.y))); + } + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint( + label.x - Tile.TILE_SIZE, label.y))); + + toAdd.addTile(left); + } + + if ((label.symbol.x + label.symbol.symbol.getWidth() >= Tile.TILE_SIZE) + && (!this.dependencyTable.get(right).drawn)) { + linkedDep = this.dependencyTable.get(right); + + if (toAdd == null) { + toAdd = new DependencyText(label.paintFront, label.paintBack, label.text, label.boundary, + this.currentTile); + + this.currentDependencyOnTile.addText(new Dependency(toAdd, new ImmutablePoint( + label.x, label.y))); + } + + linkedDep.addText(new Dependency(toAdd, new ImmutablePoint( + label.x + Tile.TILE_SIZE, label.y))); + + toAdd.addTile(right); + } + } + } + } + + private void fillDependencyOnTile2(List labels, List symbols, + List areaLabels) { + Tile left = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile right = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile up = new Tile(this.currentTile.tileX, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile down = new Tile(this.currentTile.tileX, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + + Tile leftup = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile leftdown = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + Tile rightup = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile rightdown = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + + if (this.dependencyTable.get(up) == null) { + this.dependencyTable.put(up, new DependencyOnTile()); + } + if (this.dependencyTable.get(down) == null) { + this.dependencyTable.put(down, new DependencyOnTile()); + } + if (this.dependencyTable.get(left) == null) { + this.dependencyTable.put(left, new DependencyOnTile()); + } + if (this.dependencyTable.get(right) == null) { + this.dependencyTable.put(right, new DependencyOnTile()); + } + if (this.dependencyTable.get(leftdown) == null) { + this.dependencyTable.put(leftdown, new DependencyOnTile()); + } + if (this.dependencyTable.get(rightup) == null) { + this.dependencyTable.put(rightup, new DependencyOnTile()); + } + if (this.dependencyTable.get(leftup) == null) { + this.dependencyTable.put(leftup, new DependencyOnTile()); + } + if (this.dependencyTable.get(rightdown) == null) { + this.dependencyTable.put(rightdown, new DependencyOnTile()); + } + + fillDependencyLabels(labels); + fillDependencyLabels(areaLabels); + + DependencyOnTile linkedDep; + DependencySymbol addSmb; + + for (SymbolContainer symbol : symbols) { + addSmb = null; + + // up + if ((symbol.y < 0.0f) && (!this.dependencyTable.get(up).drawn)) { + linkedDep = this.dependencyTable.get(up); + + addSmb = new DependencySymbol(symbol.symbol, this.currentTile); + this.currentDependencyOnTile.addSymbol(new Dependency(addSmb, new ImmutablePoint( + symbol.x, symbol.y))); + + linkedDep.addSymbol(new Dependency(addSmb, new ImmutablePoint(symbol.x, symbol.y + + Tile.TILE_SIZE))); + addSmb.addTile(up); + + if ((symbol.x < 0.0f) && (!this.dependencyTable.get(leftup).drawn)) { + linkedDep = this.dependencyTable.get(leftup); + + linkedDep.addSymbol(new Dependency(addSmb, new ImmutablePoint(symbol.x + + Tile.TILE_SIZE, symbol.y + Tile.TILE_SIZE))); + addSmb.addTile(leftup); + } + + if ((symbol.x + symbol.symbol.getWidth() > Tile.TILE_SIZE) + && (!this.dependencyTable.get(rightup).drawn)) { + linkedDep = this.dependencyTable.get(rightup); + + linkedDep.addSymbol(new Dependency(addSmb, new ImmutablePoint(symbol.x + - Tile.TILE_SIZE, symbol.y + Tile.TILE_SIZE))); + addSmb.addTile(rightup); + } + } + + // down + if ((symbol.y + symbol.symbol.getHeight() > Tile.TILE_SIZE) && (!this.dependencyTable.get(down).drawn)) { + + linkedDep = this.dependencyTable.get(down); + + if (addSmb == null) { + addSmb = new DependencySymbol(symbol.symbol, this.currentTile); + this.currentDependencyOnTile.addSymbol(new Dependency(addSmb, new ImmutablePoint( + symbol.x, symbol.y))); + } + + linkedDep.addSymbol(new Dependency(addSmb, new ImmutablePoint(symbol.x, symbol.y + - Tile.TILE_SIZE))); + addSmb.addTile(down); + + if ((symbol.x < 0.0f) && (!this.dependencyTable.get(leftdown).drawn)) { + linkedDep = this.dependencyTable.get(leftdown); + + linkedDep.addSymbol(new Dependency(addSmb, new ImmutablePoint(symbol.x + + Tile.TILE_SIZE, symbol.y - Tile.TILE_SIZE))); + addSmb.addTile(leftdown); + } + + if ((symbol.x + symbol.symbol.getWidth() > Tile.TILE_SIZE) + && (!this.dependencyTable.get(rightdown).drawn)) { + + linkedDep = this.dependencyTable.get(rightdown); + + linkedDep.addSymbol(new Dependency(addSmb, new ImmutablePoint(symbol.x + - Tile.TILE_SIZE, symbol.y - Tile.TILE_SIZE))); + addSmb.addTile(rightdown); + } + } + + // left + if ((symbol.x < 0.0f) && (!this.dependencyTable.get(left).drawn)) { + linkedDep = this.dependencyTable.get(left); + + if (addSmb == null) { + addSmb = new DependencySymbol(symbol.symbol, this.currentTile); + this.currentDependencyOnTile.addSymbol(new Dependency(addSmb, new ImmutablePoint( + symbol.x, symbol.y))); + } + + linkedDep.addSymbol(new Dependency(addSmb, new ImmutablePoint(symbol.x + + Tile.TILE_SIZE, symbol.y))); + addSmb.addTile(left); + } + + // right + if ((symbol.x + symbol.symbol.getWidth() > Tile.TILE_SIZE) && (!this.dependencyTable.get(right).drawn)) { + linkedDep = this.dependencyTable.get(right); + if (addSmb == null) { + addSmb = new DependencySymbol(symbol.symbol, this.currentTile); + this.currentDependencyOnTile.addSymbol(new Dependency(addSmb, new ImmutablePoint( + symbol.x, symbol.y))); + } + + linkedDep.addSymbol(new Dependency(addSmb, new ImmutablePoint(symbol.x + - Tile.TILE_SIZE, symbol.y))); + addSmb.addTile(right); + } + } + } + + private void removeOverlappingAreaLabelsWithDependencyLabels(List areaLabels) { + PointTextContainer pTC; + + for (int i = 0; i < this.currentDependencyOnTile.labels.size(); i++) { + this.depLabel = this.currentDependencyOnTile.labels.get(i); + this.rect1 = new android.graphics.Rect((int) (this.depLabel.point.pointX), + (int) (this.depLabel.point.pointY - this.depLabel.value.boundary.height()), + (int) (this.depLabel.point.pointX + this.depLabel.value.boundary.width()), + (int) (this.depLabel.point.pointY)); + + for (int x = 0; x < areaLabels.size(); x++) { + pTC = areaLabels.get(x); + + this.rect2 = new android.graphics.Rect((int) pTC.x, (int) pTC.y - pTC.boundary.height(), (int) pTC.x + + pTC.boundary.width(), (int) pTC.y); + + if (android.graphics.Rect.intersects(this.rect2, this.rect1)) { + areaLabels.remove(x); + x--; + } + } + } + } + + private void removeOverlappingAreaLabelsWithDependencySymbols(List areaLabels) { + PointTextContainer label; + + for (Dependency depSmb : this.currentDependencyOnTile.symbols) { + + this.rect1 = new android.graphics.Rect((int) depSmb.point.pointX, (int) depSmb.point.pointY, + (int) depSmb.point.pointX + depSmb.value.symbol.getWidth(), (int) depSmb.point.pointY + + depSmb.value.symbol.getHeight()); + + for (int x = 0; x < areaLabels.size(); x++) { + label = areaLabels.get(x); + + this.rect2 = new android.graphics.Rect((int) (label.x), (int) (label.y - label.boundary.height()), + (int) (label.x + label.boundary.width()), (int) (label.y)); + + if (android.graphics.Rect.intersects(this.rect2, this.rect1)) { + areaLabels.remove(x); + x--; + } + } + } + } + + private void removeOverlappingLabelsWithDependencyLabels(List labels) { + for (int i = 0; i < this.currentDependencyOnTile.labels.size(); i++) { + for (int x = 0; x < labels.size(); x++) { + if ((labels.get(x).text.equals(this.currentDependencyOnTile.labels.get(i).value.text)) + && (labels.get(x).paintFront + .equals(this.currentDependencyOnTile.labels.get(i).value.paintFront)) + && (labels.get(x).paintBack.equals(this.currentDependencyOnTile.labels.get(i).value.paintBack))) { + labels.remove(x); + i--; + break; + } + } + } + } + + private void removeOverlappingSymbolsWithDepencySymbols(List symbols, int dis) { + SymbolContainer sym; + Dependency sym2; + + for (int x = 0; x < this.currentDependencyOnTile.symbols.size(); x++) { + sym2 = this.currentDependencyOnTile.symbols.get(x); + this.rect1 = new android.graphics.Rect((int) sym2.point.pointX - dis, (int) sym2.point.pointY - dis, + (int) sym2.point.pointX + sym2.value.symbol.getWidth() + dis, (int) sym2.point.pointY + + sym2.value.symbol.getHeight() + dis); + + for (int y = 0; y < symbols.size(); y++) { + + sym = symbols.get(y); + this.rect2 = new android.graphics.Rect((int) sym.x, (int) sym.y, (int) sym.x + sym.symbol.getWidth(), + (int) sym.y + sym.symbol.getHeight()); + + if (android.graphics.Rect.intersects(this.rect2, this.rect1)) { + symbols.remove(y); + y--; + } + } + } + } + + private void removeOverlappingSymbolsWithDependencyLabels(List symbols) { + for (int i = 0; i < this.currentDependencyOnTile.labels.size(); i++) { + this.depLabel = this.currentDependencyOnTile.labels.get(i); + this.rect1 = new android.graphics.Rect((int) (this.depLabel.point.pointX), + (int) (this.depLabel.point.pointY - this.depLabel.value.boundary.height()), + (int) (this.depLabel.point.pointX + this.depLabel.value.boundary.width()), + (int) (this.depLabel.point.pointY)); + + for (int x = 0; x < symbols.size(); x++) { + this.smb = symbols.get(x); + + this.rect2 = new android.graphics.Rect((int) this.smb.x, (int) this.smb.y, (int) this.smb.x + + this.smb.symbol.getWidth(), (int) this.smb.y + this.smb.symbol.getHeight()); + + if (android.graphics.Rect.intersects(this.rect2, this.rect1)) { + symbols.remove(x); + x--; + } + } + } + } + + /** + * This method fills the entries in the dependency cache of the tiles, if their dependencies. + * + * @param labels + * current labels, that will be displayed. + * @param symbols + * current symbols, that will be displayed. + * @param areaLabels + * current areaLabels, that will be displayed. + */ + void fillDependencyOnTile(List labels, List symbols, + List areaLabels) { + this.currentDependencyOnTile.drawn = true; + + if ((!labels.isEmpty()) || (!symbols.isEmpty()) || (!areaLabels.isEmpty())) { + fillDependencyOnTile2(labels, symbols, areaLabels); + } + + if (this.currentDependencyOnTile.labels != null) { + addLabelsFromDependencyOnTile(labels); + } + if (this.currentDependencyOnTile.symbols != null) { + addSymbolsFromDependencyOnTile(symbols); + } + } + + /** + * This method must be called, before the dependencies will be handled correctly. Because it sets the actual Tile + * and looks if it has already dependencies. + * + * @param tile + * the current Tile + */ + void generateTileAndDependencyOnTile(Tile tile) { + this.currentTile = new Tile(tile.tileX, tile.tileY, tile.zoomLevel); + this.currentDependencyOnTile = this.dependencyTable.get(this.currentTile); + + if (this.currentDependencyOnTile == null) { + this.dependencyTable.put(this.currentTile, new DependencyOnTile()); + this.currentDependencyOnTile = this.dependencyTable.get(this.currentTile); + } + } + + /** + * Removes the are labels from the actual list, that would be rendered in a Tile that has already be drawn. + * + * @param areaLabels + * current area Labels, that will be displayed + */ + void removeAreaLabelsInAlreadyDrawnAreas(List areaLabels) { + Tile lefttmp = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile righttmp = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile uptmp = new Tile(this.currentTile.tileX, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile downtmp = new Tile(this.currentTile.tileX, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + + boolean up; + boolean left; + boolean right; + boolean down; + + this.tmp = this.dependencyTable.get(lefttmp); + left = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(righttmp); + right = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(uptmp); + up = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(downtmp); + down = this.tmp == null ? false : this.tmp.drawn; + + PointTextContainer label; + + for (int i = 0; i < areaLabels.size(); i++) { + label = areaLabels.get(i); + + if (up && label.y - label.boundary.height() < 0.0f) { + areaLabels.remove(i); + i--; + continue; + } + + if (down && label.y > Tile.TILE_SIZE) { + areaLabels.remove(i); + i--; + continue; + } + if (left && label.x < 0.0f) { + areaLabels.remove(i); + i--; + continue; + } + if (right && label.x + label.boundary.width() > Tile.TILE_SIZE) { + areaLabels.remove(i); + i--; + continue; + } + } + } + + /** + * Removes all objects that overlaps with the objects from the dependency cache. + * + * @param labels + * labels from the current tile + * @param areaLabels + * area labels from the current tile + * @param symbols + * symbols from the current tile + */ + void removeOverlappingObjectsWithDependencyOnTile(List labels, + List areaLabels, List symbols) { + if (this.currentDependencyOnTile.labels != null && this.currentDependencyOnTile.labels.size() != 0) { + removeOverlappingLabelsWithDependencyLabels(labels); + removeOverlappingSymbolsWithDependencyLabels(symbols); + removeOverlappingAreaLabelsWithDependencyLabels(areaLabels); + } + + if (this.currentDependencyOnTile.symbols != null && this.currentDependencyOnTile.symbols.size() != 0) { + removeOverlappingSymbolsWithDepencySymbols(symbols, 2); + removeOverlappingAreaLabelsWithDependencySymbols(areaLabels); + } + } + + /** + * When the LabelPlacement class generates potential label positions for an POI, there should be no possible + * positions, that collide with existing symbols or labels in the dependency Cache. This class implements this + * functionality. + * + * @param refPos + * possible label positions form the two or four point Greedy + */ + void removeReferencePointsFromDependencyCache(LabelPlacement.ReferencePosition[] refPos) { + Tile lefttmp = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile righttmp = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile uptmp = new Tile(this.currentTile.tileX, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile downtmp = new Tile(this.currentTile.tileX, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + + boolean up; + boolean left; + boolean right; + boolean down; + + this.tmp = this.dependencyTable.get(lefttmp); + left = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(righttmp); + right = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(uptmp); + up = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(downtmp); + down = this.tmp == null ? false : this.tmp.drawn; + + LabelPlacement.ReferencePosition ref; + + for (int i = 0; i < refPos.length; i++) { + ref = refPos[i]; + + if (ref == null) { + continue; + } + + if (up && ref.y - ref.height < 0) { + refPos[i] = null; + continue; + } + + if (down && ref.y >= Tile.TILE_SIZE) { + refPos[i] = null; + continue; + } + + if (left && ref.x < 0) { + refPos[i] = null; + continue; + } + + if (right && ref.x + ref.width > Tile.TILE_SIZE) { + refPos[i] = null; + } + } + + // removes all Reverence Points that intersects with Labels from the Dependency Cache + + int dis = 2; + if (this.currentDependencyOnTile != null) { + if (this.currentDependencyOnTile.labels != null) { + for (int i = 0; i < this.currentDependencyOnTile.labels.size(); i++) { + this.depLabel = this.currentDependencyOnTile.labels.get(i); + this.rect1 = new android.graphics.Rect((int) this.depLabel.point.pointX - dis, + (int) (this.depLabel.point.pointY - this.depLabel.value.boundary.height()) - dis, + (int) (this.depLabel.point.pointX + this.depLabel.value.boundary.width() + dis), + (int) (this.depLabel.point.pointY + dis)); + + for (int y = 0; y < refPos.length; y++) { + if (refPos[y] != null) { + this.rect2 = new android.graphics.Rect((int) refPos[y].x, + (int) (refPos[y].y - refPos[y].height), (int) (refPos[y].x + refPos[y].width), + (int) (refPos[y].y)); + + if (android.graphics.Rect.intersects(this.rect2, this.rect1)) { + refPos[y] = null; + } + } + } + } + } + if (this.currentDependencyOnTile.symbols != null) { + for (Dependency symbols2 : this.currentDependencyOnTile.symbols) { + + this.rect1 = new android.graphics.Rect((int) symbols2.point.pointX, (int) (symbols2.point.pointY), + (int) (symbols2.point.pointX + symbols2.value.symbol.getWidth()), + (int) (symbols2.point.pointY + symbols2.value.symbol.getHeight())); + + for (int y = 0; y < refPos.length; y++) { + if (refPos[y] != null) { + this.rect2 = new android.graphics.Rect((int) refPos[y].x, + (int) (refPos[y].y - refPos[y].height), (int) (refPos[y].x + refPos[y].width), + (int) (refPos[y].y)); + + if (android.graphics.Rect.intersects(this.rect2, this.rect1)) { + refPos[y] = null; + } + } + } + } + } + } + } + + void removeSymbolsFromDrawnAreas(List symbols) { + Tile lefttmp = new Tile(this.currentTile.tileX - 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile righttmp = new Tile(this.currentTile.tileX + 1, this.currentTile.tileY, this.currentTile.zoomLevel); + Tile uptmp = new Tile(this.currentTile.tileX, this.currentTile.tileY - 1, this.currentTile.zoomLevel); + Tile downtmp = new Tile(this.currentTile.tileX, this.currentTile.tileY + 1, this.currentTile.zoomLevel); + + boolean up; + boolean left; + boolean right; + boolean down; + + this.tmp = this.dependencyTable.get(lefttmp); + left = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(righttmp); + right = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(uptmp); + up = this.tmp == null ? false : this.tmp.drawn; + + this.tmp = this.dependencyTable.get(downtmp); + down = this.tmp == null ? false : this.tmp.drawn; + + SymbolContainer ref; + + for (int i = 0; i < symbols.size(); i++) { + ref = symbols.get(i); + + if (up && ref.y < 0) { + symbols.remove(i); + i--; + continue; + } + + if (down && ref.y + ref.symbol.getHeight() > Tile.TILE_SIZE) { + symbols.remove(i); + i--; + continue; + } + if (left && ref.x < 0) { + symbols.remove(i); + i--; + continue; + } + if (right && ref.x + ref.symbol.getWidth() > Tile.TILE_SIZE) { + symbols.remove(i); + i--; + continue; + } + } + } +} diff --git a/src/org/mapsforge/android/swrenderer/GLMapTile.java b/src/org/mapsforge/android/swrenderer/GLMapTile.java new file mode 100644 index 00000000..26486a1b --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/GLMapTile.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import org.mapsforge.android.mapgenerator.MapTile; + +/** + * + */ +public class GLMapTile extends MapTile { + private float mScale; + + final GLMapTile[] child = { null, null, null, null }; + GLMapTile parent; + + // private long mLoadTime; + private int mTextureID; + + /** + * @param tileX + * ... + * @param tileY + * ... + * @param zoomLevel + * .. + */ + public GLMapTile(long tileX, long tileY, byte zoomLevel) { + super(tileX, tileY, zoomLevel); + mScale = 1; + isDrawn = false; + mTextureID = -1; + } + + /** + * @return ... + */ + public int getTexture() { + return mTextureID; + } + + /** + * @param mTextureID + * ... + */ + public void setTexture(int mTextureID) { + this.mTextureID = mTextureID; + } + + /** + * @return ... + */ + public boolean hasTexture() { + return mTextureID >= 0; + } + + /** + * @return ... + */ + public float getScale() { + return mScale; + } + + /** + * @param scale + * ... + */ + public void setScale(float scale) { + mScale = scale; + } + +} diff --git a/src/org/mapsforge/android/swrenderer/ImmutablePoint.java b/src/org/mapsforge/android/swrenderer/ImmutablePoint.java new file mode 100644 index 00000000..da5e8020 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/ImmutablePoint.java @@ -0,0 +1,116 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +/** + * An ImmutablePoint represents an fixed pair of float coordinates. + */ +class ImmutablePoint implements Comparable { + /** + * Subtracts the x and y coordinates of one point from another point. + * + * @param minuend + * the minuend. + * @param subtrahend + * the subtrahend. + * @return a new Point object. + */ + static ImmutablePoint substract(ImmutablePoint minuend, ImmutablePoint subtrahend) { + return new ImmutablePoint(minuend.pointX - subtrahend.pointX, minuend.pointY - subtrahend.pointY); + } + + /** + * Stores the hash code of this object. + */ + private final int hashCodeValue; + + /** + * X coordinate of this point. + */ + final float pointX; + + /** + * Y coordinate of this point. + */ + final float pointY; + + /** + * @param x + * the x coordinate of the point. + * @param y + * the y coordinate of the point. + */ + ImmutablePoint(float x, float y) { + this.pointX = x; + this.pointY = y; + this.hashCodeValue = calculateHashCode(); + } + + @Override + public int compareTo(ImmutablePoint point) { + if (this.pointX > point.pointX) { + return 1; + } else if (this.pointX < point.pointX) { + return -1; + } else if (this.pointY > point.pointY) { + return 1; + } else if (this.pointY < point.pointY) { + return -1; + } + return 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof ImmutablePoint)) { + return false; + } + ImmutablePoint other = (ImmutablePoint) obj; + if (this.pointX != other.pointX) { + return false; + } else if (this.pointY != other.pointY) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return this.hashCodeValue; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("ImmutablePoint [x="); + stringBuilder.append(this.pointX); + stringBuilder.append(", y="); + stringBuilder.append(this.pointY); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + Float.floatToIntBits(this.pointX); + result = 31 * result + Float.floatToIntBits(this.pointY); + return result; + } +} diff --git a/src/org/mapsforge/android/swrenderer/LabelPlacement.java b/src/org/mapsforge/android/swrenderer/LabelPlacement.java new file mode 100644 index 00000000..177b8bfe --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/LabelPlacement.java @@ -0,0 +1,766 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.PriorityQueue; + +import org.mapsforge.core.Tile; + +import android.graphics.Rect; + +/** + * This class place the labels form POIs, area labels and normal labels. The main target is avoiding collisions of these + * different labels. + */ +class LabelPlacement { + /** + * This class holds the reference positions for the two and four point greedy algorithms. + */ + static class ReferencePosition { + final float height; + final int nodeNumber; + SymbolContainer symbol; + final float width; + final float x; + final float y; + + ReferencePosition(float x, float y, int nodeNumber, float width, float height, SymbolContainer symbol) { + this.x = x; + this.y = y; + this.nodeNumber = nodeNumber; + this.width = width; + this.height = height; + this.symbol = symbol; + } + } + + static final class ReferencePositionHeightComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + static final ReferencePositionHeightComparator INSTANCE = new ReferencePositionHeightComparator(); + + private ReferencePositionHeightComparator() { + // do nothing + } + + @Override + public int compare(ReferencePosition x, ReferencePosition y) { + if (x.y - x.height < y.y - y.height) { + return -1; + } + + if (x.y - x.height > y.y - y.height) { + return 1; + } + return 0; + } + } + + static final class ReferencePositionWidthComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + static final ReferencePositionWidthComparator INSTANCE = new ReferencePositionWidthComparator(); + + private ReferencePositionWidthComparator() { + // do nothing + } + + @Override + public int compare(ReferencePosition x, ReferencePosition y) { + if (x.x + x.width < y.x + y.width) { + return -1; + } + + if (x.x + x.width > y.x + y.width) { + return 1; + } + + return 0; + } + } + + static final class ReferencePositionXComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + static final ReferencePositionXComparator INSTANCE = new ReferencePositionXComparator(); + + private ReferencePositionXComparator() { + // do nothing + } + + @Override + public int compare(ReferencePosition x, ReferencePosition y) { + if (x.x < y.x) { + return -1; + } + + if (x.x > y.x) { + return 1; + } + + return 0; + } + } + + static final class ReferencePositionYComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + static final ReferencePositionYComparator INSTANCE = new ReferencePositionYComparator(); + + private ReferencePositionYComparator() { + // do nothing + } + + @Override + public int compare(ReferencePosition x, ReferencePosition y) { + if (x.y < y.y) { + return -1; + } + + if (x.y > y.y) { + return 1; + } + + return 0; + } + } + + private static final int PLACEMENT_MODEL = 1; + private int mLabelDistanceToLabel = 2; + private int mLabelDistanceToSymbol = 2; + // You can choose between 2 Position and 4 Position + // placement Model 0 - 2-Position 1 - 4 Position + // distance adjustments + private int mStartDistanceToSymbols = 4; + private int mSymbolDistanceToSymbol = 2; + + final DependencyCache mDependencyCache; + PointTextContainer mLabel; + Rect mRect1; + Rect mRect2; + ReferencePosition mReferencePosition; + SymbolContainer mSymbolContainer; + + LabelPlacement() { + mDependencyCache = new DependencyCache(); + mRect1 = new Rect(); + mRect2 = new Rect(); + } + + /** + * Centers the labels. + * + * @param labels + * labels to center + */ + private void centerLabels(List labels) { + for (int i = 0; i < labels.size(); i++) { + mLabel = labels.get(i); + mLabel.x = mLabel.x - mLabel.boundary.width() / 2; + } + } + + private void preprocessAreaLabels(List areaLabels) { + centerLabels(areaLabels); + + removeOutOfTileAreaLabels(areaLabels); + + removeOverlappingAreaLabels(areaLabels); + + if (!areaLabels.isEmpty()) { + mDependencyCache.removeAreaLabelsInAlreadyDrawnAreas(areaLabels); + } + } + + private void preprocessLabels(List labels) { + removeOutOfTileLabels(labels); + } + + private void preprocessSymbols(List symbols) { + removeOutOfTileSymbols(symbols); + removeOverlappingSymbols(symbols); + mDependencyCache.removeSymbolsFromDrawnAreas(symbols); + } + + /** + * This method uses an adapted greedy strategy for the fixed four position model, above, under left and right form + * the point of interest. It uses no priority search tree, because it will not function with symbols only with + * points. Instead it uses two minimum heaps. They work similar to a sweep line algorithm but have not a O(n log n + * +k) runtime. To find the rectangle that has the top edge, I use also a minimum Heap. The rectangles are sorted by + * their y coordinates. + * + * @param labels + * label positions and text + * @param symbols + * symbol positions + * @param areaLabels + * area label positions and text + * @return list of labels without overlaps with symbols and other labels by the four fixed position greedy strategy + */ + private List processFourPointGreedy(List labels, + List symbols, List areaLabels) { + List resolutionSet = new ArrayList(); + + // Array for the generated reference positions around the points of + // interests + ReferencePosition[] refPos = new ReferencePosition[(labels.size()) * 4]; + + // lists that sorts the reference points after the minimum top edge y + // position + PriorityQueue priorUp = new PriorityQueue(labels.size() * 4 * 2 + + labels.size() / 10 * 2, ReferencePositionYComparator.INSTANCE); + // lists that sorts the reference points after the minimum bottom edge y + // position + PriorityQueue priorDown = new PriorityQueue(labels.size() * 4 * 2 + + labels.size() / 10 * 2, ReferencePositionHeightComparator.INSTANCE); + + PointTextContainer tmp; + int dis = mStartDistanceToSymbols; + + // creates the reference positions + for (int z = 0; z < labels.size(); z++) { + if (labels.get(z) != null) { + if (labels.get(z).symbol != null) { + tmp = labels.get(z); + + // up + refPos[z * 4] = new ReferencePosition(tmp.x - tmp.boundary.width() / 2, tmp.y + - tmp.symbol.symbol.getHeight() / 2 - dis, z, tmp.boundary.width(), tmp.boundary.height(), + tmp.symbol); + // down + refPos[z * 4 + 1] = new ReferencePosition(tmp.x - tmp.boundary.width() / 2, tmp.y + + tmp.symbol.symbol.getHeight() / 2 + tmp.boundary.height() + dis, z, tmp.boundary.width(), + tmp.boundary.height(), tmp.symbol); + // left + refPos[z * 4 + 2] = new ReferencePosition(tmp.x - tmp.symbol.symbol.getWidth() / 2 + - tmp.boundary.width() - dis, tmp.y + tmp.boundary.height() / 2, z, tmp.boundary.width(), + tmp.boundary.height(), tmp.symbol); + // right + refPos[z * 4 + 3] = new ReferencePosition(tmp.x + tmp.symbol.symbol.getWidth() / 2 + dis, tmp.y + + tmp.boundary.height() / 2 - 0.1f, z, tmp.boundary.width(), tmp.boundary.height(), + tmp.symbol); + } else { + refPos[z * 4] = new ReferencePosition(labels.get(z).x - ((labels.get(z).boundary.width()) / 2), + labels.get(z).y, z, labels.get(z).boundary.width(), labels.get(z).boundary.height(), null); + refPos[z * 4 + 1] = null; + refPos[z * 4 + 2] = null; + refPos[z * 4 + 3] = null; + } + } + } + + removeNonValidateReferencePosition(refPos, symbols, areaLabels); + + // do while it gives reference positions + for (int i = 0; i < refPos.length; i++) { + mReferencePosition = refPos[i]; + if (mReferencePosition != null) { + priorUp.add(mReferencePosition); + priorDown.add(mReferencePosition); + } + } + + while (priorUp.size() != 0) { + mReferencePosition = priorUp.remove(); + + mLabel = labels.get(mReferencePosition.nodeNumber); + + resolutionSet.add(new PointTextContainer(mLabel.text, mReferencePosition.x, + mReferencePosition.y, mLabel.paintFront, mLabel.paintBack, mLabel.symbol)); + + if (priorUp.size() == 0) { + return resolutionSet; + } + + priorUp.remove(refPos[mReferencePosition.nodeNumber * 4 + 0]); + priorUp.remove(refPos[mReferencePosition.nodeNumber * 4 + 1]); + priorUp.remove(refPos[mReferencePosition.nodeNumber * 4 + 2]); + priorUp.remove(refPos[mReferencePosition.nodeNumber * 4 + 3]); + + priorDown.remove(refPos[mReferencePosition.nodeNumber * 4 + 0]); + priorDown.remove(refPos[mReferencePosition.nodeNumber * 4 + 1]); + priorDown.remove(refPos[mReferencePosition.nodeNumber * 4 + 2]); + priorDown.remove(refPos[mReferencePosition.nodeNumber * 4 + 3]); + + LinkedList linkedRef = new LinkedList(); + + while (priorDown.size() != 0) { + if (priorDown.peek().x < mReferencePosition.x + mReferencePosition.width) { + linkedRef.add(priorDown.remove()); + } else { + break; + } + } + // brute Force collision test (faster then sweep line for a small + // amount of + // objects) + for (int i = 0; i < linkedRef.size(); i++) { + if ((linkedRef.get(i).x <= mReferencePosition.x + mReferencePosition.width) + && (linkedRef.get(i).y >= mReferencePosition.y - linkedRef.get(i).height) + && (linkedRef.get(i).y <= mReferencePosition.y + linkedRef.get(i).height)) { + priorUp.remove(linkedRef.get(i)); + linkedRef.remove(i); + i--; + } + } + priorDown.addAll(linkedRef); + } + + return resolutionSet; + } + + /** + * This method uses an adapted greedy strategy for the fixed two position model, above and under. It uses no + * priority search tree, because it will not function with symbols only with points. Instead it uses two minimum + * heaps. They work similar to a sweep line algorithm but have not a O(n log n +k) runtime. To find the rectangle + * that has the leftest edge, I use also a minimum Heap. The rectangles are sorted by their x coordinates. + * + * @param labels + * label positions and text + * @param symbols + * symbol positions + * @param areaLabels + * area label positions and text + * @return list of labels without overlaps with symbols and other labels by the two fixed position greedy strategy + */ + private List processTwoPointGreedy(List labels, + List symbols, List areaLabels) { + List resolutionSet = new ArrayList(); + // Array for the generated reference positions around the points of + // interests + ReferencePosition[] refPos = new ReferencePosition[labels.size() * 2]; + + // lists that sorts the reference points after the minimum right edge x + // position + PriorityQueue priorRight = new PriorityQueue(labels.size() * 2 + + labels.size() / 10 * 2, ReferencePositionWidthComparator.INSTANCE); + // lists that sorts the reference points after the minimum left edge x + // position + PriorityQueue priorLeft = new PriorityQueue(labels.size() * 2 + + labels.size() / 10 * 2, ReferencePositionXComparator.INSTANCE); + + // creates the reference positions + for (int z = 0; z < labels.size(); z++) { + mLabel = labels.get(z); + + if (mLabel.symbol != null) { + refPos[z * 2] = new ReferencePosition(mLabel.x - (mLabel.boundary.width() / 2) - 0.1f, + mLabel.y - mLabel.boundary.height() - mStartDistanceToSymbols, z, + mLabel.boundary.width(), mLabel.boundary.height(), mLabel.symbol); + refPos[z * 2 + 1] = new ReferencePosition(mLabel.x - (mLabel.boundary.width() / 2), + mLabel.y + mLabel.symbol.symbol.getHeight() + mStartDistanceToSymbols, z, + mLabel.boundary.width(), mLabel.boundary.height(), mLabel.symbol); + } else { + refPos[z * 2] = new ReferencePosition(mLabel.x - (mLabel.boundary.width() / 2) - 0.1f, + mLabel.y, z, mLabel.boundary.width(), mLabel.boundary.height(), null); + refPos[z * 2 + 1] = null; + } + } + + // removes reference positions that overlaps with other symbols or + // dependency objects + removeNonValidateReferencePosition(refPos, symbols, areaLabels); + + for (int i = 0; i < refPos.length; i++) { + mReferencePosition = refPos[i]; + if (mReferencePosition != null) { + priorLeft.add(mReferencePosition); + priorRight.add(mReferencePosition); + } + } + + while (priorRight.size() != 0) { + mReferencePosition = priorRight.remove(); + + mLabel = labels.get(mReferencePosition.nodeNumber); + + resolutionSet.add(new PointTextContainer(mLabel.text, mReferencePosition.x, + mReferencePosition.y, mLabel.paintFront, mLabel.paintBack, + mReferencePosition.symbol)); + + // Removes the other position that is a possible position for the label + // of one point + // of interest + + priorRight.remove(refPos[mReferencePosition.nodeNumber * 2 + 1]); + + if (priorRight.size() == 0) { + return resolutionSet; + } + + priorLeft.remove(mReferencePosition); + priorLeft.remove(refPos[mReferencePosition.nodeNumber * 2 + 1]); + + // find overlapping labels and deletes the reference points and delete + // them + LinkedList linkedRef = new LinkedList(); + + while (priorLeft.size() != 0) { + if (priorLeft.peek().x < mReferencePosition.x + mReferencePosition.width) { + linkedRef.add(priorLeft.remove()); + } else { + break; + } + } + + // brute Force collision test (faster then sweep line for a small + // amount of + // objects) + for (int i = 0; i < linkedRef.size(); i++) { + if ((linkedRef.get(i).x <= mReferencePosition.x + mReferencePosition.width) + && (linkedRef.get(i).y >= mReferencePosition.y - linkedRef.get(i).height) + && (linkedRef.get(i).y <= mReferencePosition.y + linkedRef.get(i).height)) { + priorRight.remove(linkedRef.get(i)); + linkedRef.remove(i); + i--; + } + } + priorLeft.addAll(linkedRef); + } + + return resolutionSet; + } + + private void removeEmptySymbolReferences(List nodes, List symbols) { + for (int i = 0; i < nodes.size(); i++) { + mLabel = nodes.get(i); + if (!symbols.contains(mLabel.symbol)) { + mLabel.symbol = null; + } + } + } + + /** + * The greedy algorithms need possible label positions, to choose the best among them. This method removes the + * reference points, that are not validate. Not validate means, that the Reference overlap with another symbol or + * label or is outside of the tile. + * + * @param refPos + * list of the potential positions + * @param symbols + * actual list of the symbols + * @param areaLabels + * actual list of the area labels + */ + private void removeNonValidateReferencePosition(ReferencePosition[] refPos, List symbols, + List areaLabels) { + int dis = mLabelDistanceToSymbol; + + for (int i = 0; i < symbols.size(); i++) { + mSymbolContainer = symbols.get(i); + mRect1.set((int) mSymbolContainer.x - dis, (int) mSymbolContainer.y - dis, + (int) mSymbolContainer.x + mSymbolContainer.symbol.getWidth() + dis, + (int) mSymbolContainer.y + mSymbolContainer.symbol.getHeight() + dis); + + for (int y = 0; y < refPos.length; y++) { + if (refPos[y] != null) { + + mRect2.set((int) refPos[y].x, (int) (refPos[y].y - refPos[y].height), + (int) (refPos[y].x + refPos[y].width), (int) (refPos[y].y)); + + if (android.graphics.Rect.intersects(mRect2, mRect1)) { + refPos[y] = null; + } + } + } + } + + dis = mLabelDistanceToLabel; + + for (PointTextContainer areaLabel : areaLabels) { + + mRect1.set((int) areaLabel.x - dis, (int) areaLabel.y - areaLabel.boundary.height() - dis, + (int) areaLabel.x + areaLabel.boundary.width() + dis, (int) areaLabel.y + dis); + + for (int y = 0; y < refPos.length; y++) { + if (refPos[y] != null) { + + mRect2.set((int) refPos[y].x, (int) (refPos[y].y - refPos[y].height), + (int) (refPos[y].x + refPos[y].width), (int) (refPos[y].y)); + + if (android.graphics.Rect.intersects(mRect2, mRect1)) { + refPos[y] = null; + } + } + } + } + + mDependencyCache.removeReferencePointsFromDependencyCache(refPos); + } + + /** + * This method removes the area labels, that are not visible in the actual tile. + * + * @param areaLabels + * area Labels from the actual tile + */ + private void removeOutOfTileAreaLabels(List areaLabels) { + for (int i = 0; i < areaLabels.size(); i++) { + mLabel = areaLabels.get(i); + + if (mLabel.x > Tile.TILE_SIZE) { + areaLabels.remove(i); + + i--; + } else if (mLabel.y - mLabel.boundary.height() > Tile.TILE_SIZE) { + areaLabels.remove(i); + + i--; + } else if (mLabel.x + mLabel.boundary.width() < 0.0f) { + areaLabels.remove(i); + + i--; + } else if (mLabel.y + mLabel.boundary.height() < 0.0f) { + areaLabels.remove(i); + + i--; + } + } + } + + /** + * This method removes the labels, that are not visible in the actual tile. + * + * @param labels + * Labels from the actual tile + */ + private void removeOutOfTileLabels(List labels) { + for (int i = 0; i < labels.size();) { + mLabel = labels.get(i); + + if (mLabel.x - mLabel.boundary.width() / 2 > Tile.TILE_SIZE) { + labels.remove(i); + mLabel = null; + } else if (mLabel.y - mLabel.boundary.height() > Tile.TILE_SIZE) { + labels.remove(i); + mLabel = null; + } else if ((mLabel.x - mLabel.boundary.width() / 2 + mLabel.boundary.width()) < 0.0f) { + labels.remove(i); + mLabel = null; + } else if (mLabel.y < 0.0f) { + labels.remove(i); + mLabel = null; + } else { + i++; + } + } + } + + /** + * This method removes the Symbols, that are not visible in the actual tile. + * + * @param symbols + * Symbols from the actual tile + */ + private void removeOutOfTileSymbols(List symbols) { + for (int i = 0; i < symbols.size();) { + mSymbolContainer = symbols.get(i); + + if (mSymbolContainer.x > Tile.TILE_SIZE) { + symbols.remove(i); + } else if (mSymbolContainer.y > Tile.TILE_SIZE) { + symbols.remove(i); + } else if (mSymbolContainer.x + mSymbolContainer.symbol.getWidth() < 0.0f) { + symbols.remove(i); + } else if (mSymbolContainer.y + mSymbolContainer.symbol.getHeight() < 0.0f) { + symbols.remove(i); + } else { + i++; + } + } + } + + /** + * This method removes all the area labels, that overlap each other. So that the output is collision free + * + * @param areaLabels + * area labels from the actual tile + */ + private void removeOverlappingAreaLabels(List areaLabels) { + int dis = mLabelDistanceToLabel; + + for (int x = 0; x < areaLabels.size(); x++) { + mLabel = areaLabels.get(x); + mRect1.set((int) mLabel.x - dis, (int) mLabel.y - dis, + (int) (mLabel.x + mLabel.boundary.width()) + dis, + (int) (mLabel.y + mLabel.boundary.height() + dis)); + + for (int y = x + 1; y < areaLabels.size(); y++) { + if (y != x) { + mLabel = areaLabels.get(y); + mRect2.set((int) mLabel.x, (int) mLabel.y, + (int) (mLabel.x + mLabel.boundary.width()), + (int) (mLabel.y + mLabel.boundary.height())); + + if (android.graphics.Rect.intersects(mRect1, mRect2)) { + areaLabels.remove(y); + + y--; + } + } + } + } + } + + /** + * Removes the the symbols that overlap with area labels. + * + * @param symbols + * list of symbols + * @param pTC + * list of labels + */ + private void removeOverlappingSymbolsWithAreaLabels(List symbols, List pTC) { + int dis = mLabelDistanceToSymbol; + + for (int x = 0; x < pTC.size(); x++) { + mLabel = pTC.get(x); + + mRect1.set((int) mLabel.x - dis, (int) (mLabel.y - mLabel.boundary.height()) - dis, + (int) (mLabel.x + mLabel.boundary.width() + dis), (int) (mLabel.y + dis)); + + for (int y = 0; y < symbols.size(); y++) { + mSymbolContainer = symbols.get(y); + + mRect2.set((int) mSymbolContainer.x, (int) mSymbolContainer.y, + (int) (mSymbolContainer.x + mSymbolContainer.symbol.getWidth()), + (int) (mSymbolContainer.y + mSymbolContainer.symbol.getHeight())); + + if (android.graphics.Rect.intersects(mRect1, mRect2)) { + symbols.remove(y); + y--; + } + } + } + } + + int getLabelDistanceToLabel() { + return mLabelDistanceToLabel; + } + + int getLabelDistanceToSymbol() { + return mLabelDistanceToSymbol; + } + + int getPlacementOption() { + return PLACEMENT_MODEL; + } + + int getStartDistanceToSymbols() { + return mStartDistanceToSymbols; + } + + int getSymbolDistanceToSymbol() { + return mSymbolDistanceToSymbol; + } + + /** + * The inputs are all the label and symbol objects of the current tile. The output is overlap free label and symbol + * placement with the greedy strategy. The placement model is either the two fixed point or the four fixed point + * model. + * + * @param labels + * labels from the current tile. + * @param symbols + * symbols of the current tile. + * @param areaLabels + * area labels from the current tile. + * @param cT + * current tile with the x,y- coordinates and the zoom level. + * @return the processed list of labels. + */ + List placeLabels(List labels, List symbols, + List areaLabels, Tile cT) { + List returnLabels = labels; + mDependencyCache.generateTileAndDependencyOnTile(cT); + + preprocessAreaLabels(areaLabels); + + preprocessLabels(returnLabels); + + preprocessSymbols(symbols); + + removeEmptySymbolReferences(returnLabels, symbols); + + removeOverlappingSymbolsWithAreaLabels(symbols, areaLabels); + + mDependencyCache.removeOverlappingObjectsWithDependencyOnTile(returnLabels, areaLabels, symbols); + + if (!returnLabels.isEmpty()) { + switch (PLACEMENT_MODEL) { + case 0: + returnLabels = processTwoPointGreedy(returnLabels, symbols, areaLabels); + break; + case 1: + returnLabels = processFourPointGreedy(returnLabels, symbols, areaLabels); + break; + default: + break; + } + } + + mDependencyCache.fillDependencyOnTile(returnLabels, symbols, areaLabels); + + return returnLabels; + } + + /** + * This method removes all the Symbols, that overlap each other. So that the output is collision free. + * + * @param symbols + * symbols from the actual tile + */ + void removeOverlappingSymbols(List symbols) { + int dis = mSymbolDistanceToSymbol; + + for (int x = 0; x < symbols.size(); x++) { + mSymbolContainer = symbols.get(x); + mRect1.set((int) mSymbolContainer.x - dis, (int) mSymbolContainer.y - dis, + (int) mSymbolContainer.x + mSymbolContainer.symbol.getWidth() + dis, + (int) mSymbolContainer.y + mSymbolContainer.symbol.getHeight() + dis); + + for (int y = x + 1; y < symbols.size(); y++) { + if (y != x) { + mSymbolContainer = symbols.get(y); + mRect2.set((int) mSymbolContainer.x, (int) mSymbolContainer.y, + (int) mSymbolContainer.x + mSymbolContainer.symbol.getWidth(), + (int) mSymbolContainer.y + mSymbolContainer.symbol.getHeight()); + + if (android.graphics.Rect.intersects(mRect2, mRect1)) { + symbols.remove(y); + y--; + } + } + } + } + } + + void setLabelDistanceToLabel(int labelDistanceToLabel) { + mLabelDistanceToLabel = labelDistanceToLabel; + } + + void setLabelDistanceToSymbol(int labelDistanceToSymbol) { + mLabelDistanceToSymbol = labelDistanceToSymbol; + } + + void setStartDistanceToSymbols(int startDistanceToSymbols) { + mStartDistanceToSymbols = startDistanceToSymbols; + } + + void setSymbolDistanceToSymbol(int symbolDistanceToSymbol) { + mSymbolDistanceToSymbol = symbolDistanceToSymbol; + } +} diff --git a/src/org/mapsforge/android/swrenderer/LayerContainer.java b/src/org/mapsforge/android/swrenderer/LayerContainer.java new file mode 100644 index 00000000..dd7ea630 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/LayerContainer.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import java.util.List; + + +import android.graphics.Paint; + +class LayerContainer { + + final LevelContainer[] mLevels; + final boolean[] mLevelActive; + boolean mActive; + + LayerContainer(int levels) { + mLevels = new LevelContainer[levels]; + mLevelActive = new boolean[levels]; + mActive = false; + } + + List add(int level, ShapeContainer shapeContainer, Paint paint) { + mActive = true; + LevelContainer levelContainer = mLevels[level]; + if (levelContainer == null) { + levelContainer = new LevelContainer(); + mLevels[level] = levelContainer; + } + + levelContainer.add(shapeContainer, paint); + + mLevelActive[level] = true; + + return levelContainer.mShapeContainers; + } + + void clear() { + if (!mActive) + return; + + mActive = false; + + for (int level = mLevels.length - 1; level >= 0; level--) { + if (mLevelActive[level]) { + LevelContainer levelContainer = mLevels[level]; + mLevelActive[level] = false; + levelContainer.clear(); + } + } + } +} diff --git a/src/org/mapsforge/android/swrenderer/LevelContainer.java b/src/org/mapsforge/android/swrenderer/LevelContainer.java new file mode 100644 index 00000000..f0ce3c93 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/LevelContainer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import java.util.ArrayList; + + +import android.graphics.Paint; + +class LevelContainer { + final ArrayList mShapeContainers; + Paint[] mPaint; + + LevelContainer() { + mShapeContainers = new ArrayList(20); + mPaint = new Paint[2]; + } + + void add(ShapeContainer shapeContainer, Paint paint) { + if (mPaint[0] == null) + mPaint[0] = paint; + else if (mPaint[0] != paint) + mPaint[1] = paint; + + mShapeContainers.add(shapeContainer); + } + + void clear() { + mShapeContainers.clear(); + mPaint[0] = null; + mPaint[1] = null; + } +} diff --git a/src/org/mapsforge/android/swrenderer/MapRenderer.java b/src/org/mapsforge/android/swrenderer/MapRenderer.java new file mode 100644 index 00000000..20520df8 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/MapRenderer.java @@ -0,0 +1,643 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mapsforge.android.swrenderer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import org.mapsforge.android.DebugSettings; +import org.mapsforge.android.MapView; +import org.mapsforge.android.mapgenerator.JobParameters; +import org.mapsforge.android.mapgenerator.MapGenerator; +import org.mapsforge.android.mapgenerator.MapGeneratorJob; +import org.mapsforge.android.mapgenerator.MapWorker; +import org.mapsforge.android.mapgenerator.TileCacheKey; +import org.mapsforge.android.mapgenerator.TileDistanceSort; +import org.mapsforge.android.utils.GlUtils; +import org.mapsforge.core.MapPosition; +import org.mapsforge.core.MercatorProjection; +import org.mapsforge.core.Tile; + +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; + +/** + * + */ +public class MapRenderer implements org.mapsforge.android.MapRenderer { + // private static String TAG = "MapRenderer"; + + private static final int FLOAT_SIZE_BYTES = 4; + private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; + private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; + + private int mProgram; + private int muMVPMatrixHandle; + private int maPositionHandle; + private int maTextureHandle; + private int muScaleHandle; + private FloatBuffer mVertices; + private float[] mMatrix = new float[16]; + + private int mWidth, mHeight; + private double mDrawX, mDrawY; + private long mTileX, mTileY; + private float mMapScale; + private DebugSettings mDebugSettings; + private JobParameters mJobParameter; + private MapPosition mMapPosition, mPrevMapPosition; + + private ArrayList mJobList; + + ArrayList mTextures; + MapWorker mMapWorker; + MapView mMapView; + + GLMapTile[] currentTiles; + GLMapTile[] newTiles; + int currentTileCnt = 0; + + private TileCacheKey mTileCacheKey; + private LinkedHashMap mTiles; + private ArrayList mTileList; + + private boolean processedTile = true; + + private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; + + private boolean mInitial; + + private final TileDistanceSort tileDistanceSort = new TileDistanceSort(); + + /** + * @param mapView + * the MapView + */ + public MapRenderer(MapView mapView) { + mMapView = mapView; + mMapWorker = mapView.getMapWorker(); + mDebugSettings = mapView.getDebugSettings(); + mMapScale = 1; + + float[] vertices = { + 0, 0, 0, 0, 0.5f, + 0, 1, 0, 0, 0, + 1, 0, 0, 0.5f, 0.5f, + 1, 1, 0, 0.5f, 0 }; + + mVertices = ByteBuffer.allocateDirect(4 * TRIANGLE_VERTICES_DATA_STRIDE_BYTES) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + mVertices.put(vertices); + + mTextures = new ArrayList(); + mJobList = new ArrayList(); + + mTiles = new LinkedHashMap(100); + mTileList = new ArrayList(); + + mTileCacheKey = new TileCacheKey(); + mInitial = true; + } + + private void limitCache(byte zoom, int remove) { + long x = mTileX; + long y = mTileY; + int diff; + + for (GLMapTile t : mTileList) { + + diff = (t.zoomLevel - zoom); + + if (diff != 0) + { + float z = (diff > 0) ? (1 << diff) : 1.0f / (1 << -diff); + t.distance = (long) (Math.abs((t.tileX) * z - x) + Math.abs((t.tileY) * z - y)); + t.distance *= 2 * diff * diff; + } else { + t.distance = (Math.abs(t.tileX - x) + Math.abs(t.tileY - y)); + } + } + + Collections.sort(mTileList, tileDistanceSort); + + for (int j = mTileList.size() - 1, cnt = 0; cnt < remove; j--, cnt++) { + GLMapTile t = mTileList.remove(j); + + mTileCacheKey.set(t.tileX, t.tileY, t.zoomLevel); + mTiles.remove(mTileCacheKey); + + for (int i = 0; i < 4; i++) { + if (t.child[i] != null) + t.child[i].parent = null; + } + if (t.parent != null) { + for (int i = 0; i < 4; i++) { + if (t.parent.child[i] == t) + t.parent.child[i] = null; + } + } + if (t.hasTexture()) { + synchronized (mTextures) { + mTextures.add(new Integer(t.getTexture())); + } + } + } + } + + private boolean updateVisibleList(long x, long y, byte zoomLevel) { + float scale = mMapPosition.scale; + double add = 1.0f / scale; + int offsetX = (int) ((mWidth >> 1) * add); + int offsetY = (int) ((mHeight >> 1) * add); + + long pixelRight = x + offsetX; + long pixelBottom = y + offsetY; + long pixelLeft = x - offsetX; + long pixelTop = y - offsetY; + + long tileLeft = MercatorProjection.pixelXToTileX(pixelLeft, zoomLevel); + long tileTop = MercatorProjection.pixelYToTileY(pixelTop, zoomLevel); + long tileRight = MercatorProjection.pixelXToTileX(pixelRight, zoomLevel); + long tileBottom = MercatorProjection.pixelYToTileY(pixelBottom, zoomLevel); + + mJobList.clear(); + + MapGenerator mapGenerator = mMapView.getMapGenerator(); + int tiles = 0; + for (long tileY = tileTop - 1; tileY <= tileBottom + 1; tileY++) { + for (long tileX = tileLeft - 1; tileX <= tileRight + 1; tileX++) { + + GLMapTile tile = mTiles.get(mTileCacheKey.set(tileX, tileY, zoomLevel)); + + if (tile == null) { + tile = new GLMapTile(tileX, tileY, zoomLevel); + TileCacheKey key = new TileCacheKey(mTileCacheKey); + mTiles.put(key, tile); + + mTileCacheKey.set((tileX >> 1), (tileY >> 1), (byte) (zoomLevel - 1)); + tile.parent = mTiles.get(mTileCacheKey); + + long xx = tileX << 1; + long yy = tileY << 1; + byte z = (byte) (zoomLevel + 1); + + tile.child[0] = mTiles.get(mTileCacheKey.set(xx, yy, z)); + tile.child[1] = mTiles.get(mTileCacheKey.set(xx + 1, yy, z)); + tile.child[2] = mTiles.get(mTileCacheKey.set(xx, yy + 1, z)); + tile.child[3] = mTiles.get(mTileCacheKey.set(xx + 1, yy + 1, z)); + + mTileList.add(tile); + } + + newTiles[tiles++] = tile; + + if (!tile.isDrawn || (tile.getScale() != scale)) { + tile.isLoading = true; + // approximation for TileScheduler + if (tileY < tileTop || tileY > tileBottom || tileX < tileLeft || tileX > tileRight) + tile.isVisible = false; + else + tile.isVisible = true; + + MapGeneratorJob job = new MapGeneratorJob(tile, mapGenerator, + mJobParameter, mDebugSettings); + job.setScale(scale); + mJobList.add(job); + } + } + } + + synchronized (this) { + + limitCache(zoomLevel, (mTiles.size() - 200)); + + for (int i = 0; i < tiles; i++) + currentTiles[i] = newTiles[i]; + currentTileCnt = tiles; + + mDrawX = x; + mDrawY = y; + mMapScale = scale; + } + + if (mJobList.size() > 0) { + mMapView.getJobQueue().setJobs(mJobList); + synchronized (mMapWorker) { + mMapWorker.notify(); + } + } + + return true; + } + + /** + * + */ + @Override + public synchronized void redrawTiles(boolean clear) { + + boolean update = false; + + mMapPosition = mMapView.getMapPosition().getMapPosition(); + + long x = (long) MercatorProjection.longitudeToPixelX(mMapPosition.geoPoint.getLongitude(), + mMapPosition.zoomLevel); + long y = (long) MercatorProjection + .latitudeToPixelY(mMapPosition.geoPoint.getLatitude(), mMapPosition.zoomLevel); + + long tileX = MercatorProjection.pixelXToTileX(x, mMapPosition.zoomLevel); + long tileY = MercatorProjection.pixelYToTileY(y, mMapPosition.zoomLevel); + float scale = mMapPosition.scale; + + if (mInitial) { + mInitial = false; + mPrevMapPosition = mMapPosition; + mTileX = tileX; + mTileY = tileY; + update = true; + } else if (mPrevMapPosition.zoomLevel != mMapPosition.zoomLevel) { + update = true; + } else if (mMapScale != scale) { + update = true; + } else if (tileX != mTileX || tileY != mTileY) { + update = true; + } + + mTileX = tileX; + mTileY = tileY; + + if (update) { + // do not change list while drawing + // synchronized (this) { + mPrevMapPosition = mMapPosition; + updateVisibleList(x, y, mMapPosition.zoomLevel); + } + else { + synchronized (this) { + mDrawX = x; + mDrawY = y; + } + } + + mMapView.requestRender(); + } + + private MapGeneratorJob mMapGeneratorJob = null; + + @Override + public boolean passTile(MapGeneratorJob mapGeneratorJob) { + + mMapGeneratorJob = mapGeneratorJob; + processedTile = false; + mMapView.requestRender(); + + return true; + } + + private boolean drawTile(GLMapTile tile, int level, float height) { + + // do not recurse more than two parents + if (level > 2) + return true; + + if (!tile.hasTexture()) { + // draw parent below current zoom level tiles + float h = height > 0 ? height * 2 : 0.1f; + + if (level <= 2 && tile.parent != null) + return drawTile(tile.parent, level + 1, h); + + return false; + } + + float z = 1; + double drawX = mDrawX; + double drawY = mDrawY; + // translate all pixel coordinates * 'zoom factor difference' + // TODO clip tile when drawing parent + int diff = tile.zoomLevel - mMapPosition.zoomLevel; + if (diff != 0) { + if (diff > 0) { + z = (1 << diff); + } else { + z = 1.0f / (1 << -diff); + } + drawX = MercatorProjection + .longitudeToPixelX(mMapPosition.geoPoint.getLongitude(), tile.zoomLevel); + drawY = MercatorProjection + .latitudeToPixelY(mMapPosition.geoPoint.getLatitude(), tile.zoomLevel); + + } + + float mapScale = mMapScale / z; + int tileSize = Tile.TILE_SIZE; + float size = tileSize * mapScale; + + float x = (float) ((tile.pixelX) - drawX) * mapScale; + float y = (float) ((tile.pixelY + tileSize) - drawY) * mapScale; + + if (x + size < -mWidth / 2 || x > mWidth / 2) { + // Log.i(TAG, tile + " skip X " + x + " " + y); + tile.isVisible = false; + return true; + } + if (y < -mHeight / 2 || y - size > mHeight / 2) { + // Log.i(TAG, tile + " skip Y " + x + " " + y); + tile.isVisible = false; + return true; + } + + // Log.i(TAG, tile + " draw " + x + " " + y); + tile.isVisible = true; + + // set drawn tile scale (texture size) + GLES20.glUniform1f(muScaleHandle, tile.getScale()); + + Matrix.setIdentityM(mMatrix, 0); + // map tile GL coordinates to screen coordinates + Matrix.scaleM(mMatrix, 0, 2.0f * (tileSize * z) / mWidth, 2.0f * (tileSize * z) / mHeight, 1); + + // scale tile + Matrix.scaleM(mMatrix, 0, mapScale / z, mapScale / z, 1); + + // translate tile + Matrix.translateM(mMatrix, 0, (x / size), -(y / size), height); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, tile.getTexture()); + // GlUtils.checkGlError("glBindTexture"); + + GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMatrix, 0); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + return true; + } + + @Override + public void onDrawFrame(GL10 glUnused) { + boolean loadedTexture = false; + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + GLES20.glClearColor(0.95f, 0.95f, 0.94f, 1.0f); + // GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + + GLES20.glUseProgram(mProgram); + GlUtils.checkGlError("glUseProgram"); + + mVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); + GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mVertices); + GlUtils.checkGlError("glVertexAttribPointer maPosition"); + + GLES20.glEnableVertexAttribArray(maPositionHandle); + GlUtils.checkGlError("glEnableVertexAttribArray maPositionHandle"); + + mVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); + GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mVertices); + GlUtils.checkGlError("glVertexAttribPointer maTextureHandle"); + + GLES20.glEnableVertexAttribArray(maTextureHandle); + GlUtils.checkGlError("glEnableVertexAttribArray maTextureHandle"); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + + GLMapTile tile, child, child2; + + GLES20.glEnable(GLES20.GL_SCISSOR_TEST); + + // lock position and currentTiles while drawing + synchronized (this) { + if (mMapGeneratorJob != null) { + + tile = (GLMapTile) mMapGeneratorJob.tile; + // TODO tile bitmaps texture to smaller parts avoiding uploading full + // bitmap when not necessary + if (tile.getTexture() >= 0) { + // reuse tile texture + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, tile.getTexture()); + GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, mMapGeneratorJob.getBitmap()); + } else if (mTextures.size() > 0) { + // reuse texture from previous tiles + Integer texture; + texture = mTextures.remove(mTextures.size() - 1); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture.intValue()); + GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, mMapGeneratorJob.getBitmap()); + tile.setTexture(texture.intValue()); + } else { + // create texture + tile.setTexture(GlUtils.loadTextures(mMapGeneratorJob.getBitmap())); + } + + tile.setScale(mMapGeneratorJob.getScale()); + tile.isDrawn = true; + tile.isLoading = false; + + mMapGeneratorJob = null; + processedTile = true; + loadedTexture = true; + } + int tileSize = (int) (Tile.TILE_SIZE * mMapScale); + int hWidth = mWidth >> 1; + int hHeight = mHeight >> 1; + for (int i = 0, n = currentTileCnt; i < n; i++) { + tile = currentTiles[i]; + + float x = (float) (tile.pixelX - mDrawX); + float y = (float) (tile.pixelY - mDrawY); + + // clip rendering to tile boundaries + GLES20.glScissor( + hWidth + (int) (x * mMapScale) - 2, + hHeight - (int) (y * mMapScale) - tileSize - 2, + tileSize + 4, tileSize + 4); + + if (drawTile(tile, 0, 0.0f)) + continue; + + // or two zoom level above + for (int k = 0; k < 4; k++) { + if (((child = tile.child[k]) != null)) { + + if (drawTile(child, 2, 0.1f)) + continue; + + for (int j = 0; j < 4; j++) + if ((child2 = child.child[j]) != null) + drawTile(child2, 2, 0.1f); + } + } + } + } + if (loadedTexture) { + synchronized (mMapWorker) { + mMapWorker.notify(); + } + } + } + + @Override + public void onSurfaceChanged(GL10 glUnused, int width, int height) { + mWidth = width; + mHeight = height; + + int tiles = (mWidth / Tile.TILE_SIZE + 4) * (mHeight / Tile.TILE_SIZE + 4); + currentTiles = new GLMapTile[tiles]; + newTiles = new GLMapTile[tiles]; + + GLES20.glViewport(0, 0, width, height); + + mDebugSettings = mMapView.getDebugSettings(); + mJobParameter = mMapView.getJobParameters(); + + mTiles.clear(); + mTileList.clear(); + mTextures.clear(); + mInitial = true; + mMapView.redrawTiles(); + } + + @Override + public void onSurfaceCreated(GL10 glUnused, EGLConfig config) { + + mProgram = GlUtils.createProgram(mVertexShader, mFragmentShader); + if (mProgram == 0) { + return; + } + maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); + GlUtils.checkGlError("glGetAttribLocation aPosition"); + if (maPositionHandle == -1) { + throw new RuntimeException("Could not get attrib location for aPosition"); + } + maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); + GlUtils.checkGlError("glGetAttribLocation aTextureCoord"); + if (maTextureHandle == -1) { + throw new RuntimeException("Could not get attrib location for aTextureCoord"); + } + muScaleHandle = GLES20.glGetUniformLocation(mProgram, "uScale"); + GlUtils.checkGlError("glGetAttribLocation uScale"); + if (muScaleHandle == -1) { + throw new RuntimeException("Could not get attrib location for uScale"); + } + + muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + GlUtils.checkGlError("glGetUniformLocation uMVPMatrix"); + if (muMVPMatrixHandle == -1) { + throw new RuntimeException("Could not get attrib location for uMVPMatrix"); + } + + // GLES20.glEnable(GLES20.GL_DEPTH_TEST); + // GLES20.glDepthFunc(GLES20.GL_LEQUAL); + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + + GLES20.glCullFace(GLES20.GL_BACK); + GLES20.glFrontFace(GLES20.GL_CW); + GLES20.glEnable(GLES20.GL_CULL_FACE); + + } + + private final String mVertexShader = "precision highp float;\n" + + "uniform float uScale;\n" + + "uniform mat4 uMVPMatrix;\n" + "attribute vec4 aPosition;\n" + + "attribute vec2 aTextureCoord;\n" + + "varying vec2 vTextureCoord;\n" + "void main() {\n" + + " gl_Position = uMVPMatrix * aPosition;\n" + + " vTextureCoord = aTextureCoord * uScale;\n" + + "}\n"; + + private final String mFragmentShader = "precision highp float;\n" + + "uniform float uScale;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform sampler2D sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord); \n" + + "}\n"; + + @Override + public boolean processedTile() { + return processedTile; + } +} + +// private void limitCache(long top, long bottom, long left, long right, byte zoom, int remove) { +// int cnt = 0; +// TileCacheKey[] keys = new TileCacheKey[remove]; +// +// for (Entry e : mTiles.entrySet()) { +// GLMapTile t = e.getValue(); +// if (t.zoomLevel == zoom && t.tileX >= left && t.tileX <= right && +// t.tileY >= top && t.tileY <= bottom) +// continue; +// +// if (t.zoomLevel + 1 == zoom) { +// boolean found = false; +// for (int i = 0; i < 4; i++) { +// GLMapTile c = t.child[i]; +// if (c != null && !c.hasTexture() && c.tileX >= left && c.tileX <= right && +// c.tileY >= top && c.tileY <= bottom) { +// found = true; +// break; +// } +// } +// if (found) +// continue; +// } +// if (t.zoomLevel - 1 == zoom) { +// GLMapTile p = t.parent; +// if (p != null && !p.hasTexture() && p.tileX >= left && p.tileX <= right && +// p.tileY >= top && p.tileY <= bottom) { +// continue; +// } +// } +// +// keys[cnt++] = e.getKey(); +// +// if (cnt == remove) +// break; +// } +// +// for (TileCacheKey key : keys) { +// GLMapTile t = mTiles.remove(key); +// if (t == null) +// continue; +// +// for (int i = 0; i < 4; i++) { +// if (t.child[i] != null) +// t.child[i].parent = null; +// } +// if (t.parent != null) { +// for (int i = 0; i < 4; i++) { +// if (t.parent.child[i] == t) +// t.parent.child[i] = null; +// } +// } +// if (t.hasTexture()) { +// synchronized (mTextures) { +// mTextures.add(new Integer(t.getTexture())); +// } +// } +// } +// } + diff --git a/src/org/mapsforge/android/swrenderer/PointTextContainer.java b/src/org/mapsforge/android/swrenderer/PointTextContainer.java new file mode 100644 index 00000000..d33ae9c8 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/PointTextContainer.java @@ -0,0 +1,115 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import android.graphics.Paint; +import android.graphics.Rect; + +class PointTextContainer { + final Rect boundary; + final Paint paintBack; + final Paint paintFront; + SymbolContainer symbol; + final String text; + float x; + float y; + + /** + * Create a new point container, that holds the x-y coordinates of a point, a text variable and one paint objects. + * + * @param text + * the text of the point. + * @param x + * the x coordinate of the point. + * @param y + * the y coordinate of the point. + * @param paintFront + * the paintFront for the point. + */ + PointTextContainer(String text, float x, float y, Paint paintFront) { + this.text = text; + this.x = x; + this.y = y; + this.paintFront = paintFront; + this.paintBack = null; + this.symbol = null; + + this.boundary = new Rect(); + paintFront.getTextBounds(text, 0, text.length(), this.boundary); + } + + /** + * Create a new point container, that holds the x-y coordinates of a point, a text variable and two paint objects. + * + * @param text + * the text of the point. + * @param x + * the x coordinate of the point. + * @param y + * the y coordinate of the point. + * @param paintFront + * the paintFront for the point. + * @param paintBack + * the paintBack for the point. + */ + PointTextContainer(String text, float x, float y, Paint paintFront, Paint paintBack) { + this.text = text; + this.x = x; + this.y = y; + this.paintFront = paintFront; + this.paintBack = paintBack; + this.symbol = null; + + this.boundary = new Rect(); + if (paintBack != null) { + paintBack.getTextBounds(text, 0, text.length(), this.boundary); + } else { + paintFront.getTextBounds(text, 0, text.length(), this.boundary); + } + } + + /** + * Create a new point container, that holds the x-y coordinates of a point, a text variable, two paint objects, and + * a reference on a symbol, if the text is connected with a POI. + * + * @param text + * the text of the point. + * @param x + * the x coordinate of the point. + * @param y + * the y coordinate of the point. + * @param paintFront + * the paintFront for the point. + * @param paintBack + * the paintBack for the point. + * @param symbol + * the connected Symbol. + */ + PointTextContainer(String text, float x, float y, Paint paintFront, Paint paintBack, SymbolContainer symbol) { + this.text = text; + this.x = x; + this.y = y; + this.paintFront = paintFront; + this.paintBack = paintBack; + this.symbol = symbol; + + this.boundary = new Rect(); + if (paintBack != null) { + paintBack.getTextBounds(text, 0, text.length(), this.boundary); + } else { + paintFront.getTextBounds(text, 0, text.length(), this.boundary); + } + } +} diff --git a/src/org/mapsforge/android/swrenderer/ShapeContainer.java b/src/org/mapsforge/android/swrenderer/ShapeContainer.java new file mode 100644 index 00000000..7f2a92ae --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/ShapeContainer.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +interface ShapeContainer { + ShapeType getShapeType(); +} diff --git a/src/org/mapsforge/android/swrenderer/ShapePaintContainer.java b/src/org/mapsforge/android/swrenderer/ShapePaintContainer.java new file mode 100644 index 00000000..278bdc0f --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/ShapePaintContainer.java @@ -0,0 +1,29 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + + +import android.graphics.Paint; + +class ShapePaintContainer { + public Paint paint; + public ShapeContainer shapeContainer; + public ShapePaintContainer next; + + ShapePaintContainer(ShapeContainer shapeContainer, Paint paint) { + this.shapeContainer = shapeContainer; + this.paint = paint; + } +} diff --git a/src/org/mapsforge/android/swrenderer/ShapeType.java b/src/org/mapsforge/android/swrenderer/ShapeType.java new file mode 100644 index 00000000..e7679c9f --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/ShapeType.java @@ -0,0 +1,19 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +enum ShapeType { + CIRCLE, WAY; +} diff --git a/src/org/mapsforge/android/swrenderer/SymbolContainer.java b/src/org/mapsforge/android/swrenderer/SymbolContainer.java new file mode 100644 index 00000000..80b926be --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/SymbolContainer.java @@ -0,0 +1,61 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import android.graphics.Bitmap; + +class SymbolContainer { + final boolean alignCenter; + final float rotation; + final Bitmap symbol; + final float x; + final float y; + + /** + * Creates a new symbol container. The symbol will not be centered. + * + * @param symbol + * the symbol to render at the point + * @param x + * the x coordinate of the point. + * @param y + * the y coordinate of the point. + */ + SymbolContainer(Bitmap symbol, float x, float y) { + this(symbol, x, y, false, 0); + } + + /** + * Creates a new symbol container. + * + * @param symbol + * the symbol to render at the point + * @param x + * the x coordinate of the point. + * @param y + * the y coordinate of the point. + * @param alignCenter + * true if the symbol should be centered, false otherwise. + * @param rotation + * the rotation of the symbol. + */ + SymbolContainer(Bitmap symbol, float x, float y, boolean alignCenter, float rotation) { + this.symbol = symbol; + this.x = x; + this.y = y; + this.alignCenter = alignCenter; + this.rotation = rotation; + } +} diff --git a/src/org/mapsforge/android/swrenderer/WayDataContainer.java b/src/org/mapsforge/android/swrenderer/WayDataContainer.java new file mode 100644 index 00000000..5af67036 --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/WayDataContainer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +class WayDataContainer implements ShapeContainer { + // position and length of float coordinates + + int[] position; + int[] length; + + int[] textPos; + + WayDataContainer(int size) { + length = new int[size]; + position = new int[size]; + } + + @Override + public ShapeType getShapeType() { + return ShapeType.WAY; + } +} diff --git a/src/org/mapsforge/android/swrenderer/WayDecorator.java b/src/org/mapsforge/android/swrenderer/WayDecorator.java new file mode 100644 index 00000000..1fe0cb8d --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/WayDecorator.java @@ -0,0 +1,292 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + +import java.util.List; + +import org.mapsforge.android.utils.GeometryUtils; + +import android.graphics.Bitmap; +import android.graphics.Paint; +import android.util.Log; + +final class WayDecorator { + /** + * Minimum distance in pixels before the symbol is repeated. + */ + private static final int DISTANCE_BETWEEN_SYMBOLS = 200; + + /** + * Minimum distance in pixels before the way name is repeated. + */ + private static final int DISTANCE_BETWEEN_WAY_NAMES = 500; + + /** + * Distance in pixels to skip from both ends of a segment. + */ + private static final int SEGMENT_SAFETY_DISTANCE = 30; + + static void renderSymbol(Bitmap symbolBitmap, boolean alignCenter, boolean repeatSymbol, float[][] coordinates, + List waySymbols) { + int skipPixels = SEGMENT_SAFETY_DISTANCE; + + // get the first way point coordinates + float previousX = coordinates[0][0]; + float previousY = coordinates[0][1]; + + // draw the symbol on each way segment + float segmentLengthRemaining; + float segmentSkipPercentage; + float symbolAngle; + for (int i = 2; i < coordinates[0].length; i += 2) { + // get the current way point coordinates + float currentX = coordinates[0][i]; + float currentY = coordinates[0][i + 1]; + + // calculate the length of the current segment (Euclidian distance) + float diffX = currentX - previousX; + float diffY = currentY - previousY; + double segmentLengthInPixel = Math.sqrt(diffX * diffX + diffY * diffY); + segmentLengthRemaining = (float) segmentLengthInPixel; + + while (segmentLengthRemaining - skipPixels > SEGMENT_SAFETY_DISTANCE) { + // calculate the percentage of the current segment to skip + segmentSkipPercentage = skipPixels / segmentLengthRemaining; + + // move the previous point forward towards the current point + previousX += diffX * segmentSkipPercentage; + previousY += diffY * segmentSkipPercentage; + symbolAngle = (float) Math.toDegrees(Math.atan2(currentY - previousY, currentX - previousX)); + + waySymbols.add(new SymbolContainer(symbolBitmap, previousX, previousY, alignCenter, symbolAngle)); + + // check if the symbol should only be rendered once + if (!repeatSymbol) { + return; + } + + // recalculate the distances + diffX = currentX - previousX; + diffY = currentY - previousY; + + // recalculate the remaining length of the current segment + segmentLengthRemaining -= skipPixels; + + // set the amount of pixels to skip before repeating the symbol + skipPixels = DISTANCE_BETWEEN_SYMBOLS; + } + + skipPixels -= segmentLengthRemaining; + if (skipPixels < SEGMENT_SAFETY_DISTANCE) { + skipPixels = SEGMENT_SAFETY_DISTANCE; + } + + // set the previous way point coordinates for the next loop + previousX = currentX; + previousY = currentY; + } + } + + static void renderText(DatabaseRenderer databaseRenderer, Paint paint, Paint outline, + float[] coordinates, WayDataContainer wayDataContainer, + List wayNames) { + + int pos = wayDataContainer.position[0]; + int len = wayDataContainer.length[0]; + // int coordinatesLength + + String text = null; + // calculate the way name length plus some margin of safety + float wayNameWidth = -1; // paint.measureText(textKey) + 5; + float minWidth = 100; + int skipPixels = 0; + + // get the first way point coordinates + int previousX = (int) coordinates[pos + 0]; + int previousY = (int) coordinates[pos + 1]; + int containerSize = -1; + + // find way segments long enough to draw the way name on them + for (int i = pos + 2; i < pos + len; i += 2) { + // get the current way point coordinates + int currentX = (int) coordinates[i]; + int currentY = (int) coordinates[i + 1]; + int first = i - 2; + int last = i; + + // calculate the length of the current segment (Euclidian distance) + int diffX = currentX - previousX; + int diffY = currentY - previousY; + + for (int j = i + 2; j < pos + len; j += 2) { + int nextX = (int) coordinates[j]; + int nextY = (int) coordinates[j + 1]; + + if (diffY == 0) { + if ((currentY - nextY) != 0) + break; + + currentX = nextX; + currentY = nextY; + last = j; + continue; + } else if ((currentY - nextY) == 0) + break; + + float diff = ((float) (diffX) / (diffY) - (float) (currentX - nextX) / (currentY - nextY)); + + // skip segments with corners + if (diff >= 0.2 || diff <= -0.2) + break; + + currentX = nextX; + currentY = nextY; + last = j; + } + + diffX = currentX - previousX; + diffY = currentY - previousY; + + if (diffX < 0) + diffX = -diffX; + if (diffY < 0) + diffY = -diffY; + + if (diffX + diffY < minWidth) { + previousX = currentX; + previousY = currentY; + continue; + } + + if (wayNameWidth > 0 && diffX + diffY < wayNameWidth) { + previousX = currentX; + previousY = currentY; + continue; + } + + double segmentLengthInPixel = Math.sqrt(diffX * diffX + diffY * diffY); + + if (skipPixels > 0) { + skipPixels -= segmentLengthInPixel; + + } else if (segmentLengthInPixel > minWidth) { + + if (wayNameWidth < 0) { + if (text == null) { + text = databaseRenderer.getWayName(); + if (text == null) + text = "blub"; + } + + wayNameWidth = (paint.measureText(text) + 10); + } + if (segmentLengthInPixel > wayNameWidth) { + + double s = (wayNameWidth + 10) / segmentLengthInPixel; + int width, height; + int x1, y1, x2, y2; + + if (previousX < currentX) { + x1 = previousX; + y1 = previousY; + x2 = currentX; + y2 = currentY; + } else { + x1 = currentX; + y1 = currentY; + x2 = previousX; + y2 = previousY; + } + + // estimate position of test on path + width = (x2 - x1) / 2; + x2 = x2 - (int) (width - s * width); + x1 = x1 + (int) (width - s * width); + + height = (y2 - y1) / 2; + y2 = y2 - (int) (height - s * height); + y1 = y1 + (int) (height - s * height); + + short top = (short) (y1 < y2 ? y1 : y2); + short bot = (short) (y1 < y2 ? y2 : y1); + + boolean intersects = false; + + if (containerSize == -1) + containerSize = wayNames.size(); + + for (int k = 0; k < containerSize; k++) { + WayTextContainer wtc2 = wayNames.get(k); + if (!wtc2.match) + // outline (not 'match') are appended to the end; + break; + + // check crossings + if (GeometryUtils.lineIntersect(x1, y1, x2, y2, wtc2.x1, wtc2.y1, wtc2.x2, wtc2.y2)) { + intersects = true; + break; + } + + // check overlapping labels of road with more than one + // way + short top2 = (wtc2.y1 < wtc2.y2 ? wtc2.y1 : wtc2.y2); + short bot2 = (wtc2.y1 < wtc2.y2 ? wtc2.y2 : wtc2.y1); + + if (x1 - 10 < wtc2.x2 && wtc2.x1 - 10 < x2 && top - 10 < bot2 && top2 - 10 < bot) { + + if (wtc2.text.equals(text)) { + intersects = true; + break; + } + } + } + + if (intersects) { + previousX = (int) coordinates[pos + i]; + previousY = (int) coordinates[pos + i + 1]; + continue; + } + + Log.d("mapsforge", "add " + text + " " + first + " " + last); + WayTextContainer wtc = new WayTextContainer(first, last, wayDataContainer, text, + paint); + wtc.x1 = (short) x1; + wtc.y1 = (short) y1; + wtc.x2 = (short) x2; + wtc.y2 = (short) y2; + wtc.match = true; + + wayNames.add(0, wtc); + containerSize++; + + if (outline != null) { + wayNames.add(new WayTextContainer(first, last, wayDataContainer, text, outline)); + containerSize++; + } + // 500 ??? how big is a tile?! + skipPixels = DISTANCE_BETWEEN_WAY_NAMES; + } + } + + // store the previous way point coordinates + previousX = currentX; + previousY = currentY; + } + } + + private WayDecorator() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/android/swrenderer/WayTextContainer.java b/src/org/mapsforge/android/swrenderer/WayTextContainer.java new file mode 100644 index 00000000..17341b4f --- /dev/null +++ b/src/org/mapsforge/android/swrenderer/WayTextContainer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.swrenderer; + + +import android.graphics.Paint; + +class WayTextContainer { + final WayDataContainer wayDataContainer; + final int first; + final int last; + final Paint paint; + final String text; + short x1, y1, x2, y2; + boolean match; + + WayTextContainer(int first, int last, WayDataContainer wayDataContainer, String text, Paint paint) { + this.wayDataContainer = wayDataContainer; + this.first = first; + this.last = last; + this.text = text; + this.paint = paint; + this.match = false; + } +} diff --git a/src/org/mapsforge/android/utils/AndroidUtils.java b/src/org/mapsforge/android/utils/AndroidUtils.java new file mode 100644 index 00000000..e359116e --- /dev/null +++ b/src/org/mapsforge/android/utils/AndroidUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.utils; + +import android.os.Build; +import android.os.Looper; + +/** + * A utility class with Android-specific helper methods. + */ +public final class AndroidUtils { + /** + * Build names to detect the emulator from the Android SDK. + */ + private static final String[] EMULATOR_NAMES = { "google_sdk", "sdk" }; + + /** + * @return true if the application is running on the Android emulator, false otherwise. + */ + public static boolean applicationRunsOnAndroidEmulator() { + for (int i = 0, n = EMULATOR_NAMES.length; i < n; ++i) { + if (Build.PRODUCT.equals(EMULATOR_NAMES[i])) { + return true; + } + } + + return false; + } + + /** + * @return true if the current thread is the UI thread, false otherwise. + */ + public static boolean currentThreadIsUiThread() { + return Looper.getMainLooper().getThread() == Thread.currentThread(); + } + + private AndroidUtils() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/android/utils/FastMath.java b/src/org/mapsforge/android/utils/FastMath.java new file mode 100644 index 00000000..8561e984 --- /dev/null +++ b/src/org/mapsforge/android/utils/FastMath.java @@ -0,0 +1,70 @@ +package org.mapsforge.android.utils; + +/** + * Copyright (c) 2009-2010 jMonkeyEngine + * All rights reserved. FastMath.java + */ +public class FastMath { + + private static final byte b0x7c = (byte) 0x7c; + private static final byte b0x00 = (byte) 0x00; + private static final byte b0x01 = (byte) 0x01; + private static final byte b0xfc = (byte) 0xfc; + private static final byte b0x80 = (byte) 0x80; + private static final byte b0x7b = (byte) 0x7b; + private static final byte b0xff = (byte) 0xff; + private static final byte b0xfb = (byte) 0xfb; + private static final float FLOAT_HALF_PREC = 5.96046E-8f; + private static final float FLOAT_HALF_MAX = 65504f; + + /** + * @param flt + * ... + * @param data + * ... + * @param pos + * .. + */ + public static void convertFloatToHalf(float flt, byte[] data, int pos) { + if (flt == 0f) { + data[pos + 1] = b0x00; + data[pos + 0] = b0x00; + } else if (flt == -0f) { + data[pos + 1] = b0x80; + data[pos + 0] = b0x00; + } else if (flt > FLOAT_HALF_MAX) { + if (flt == Float.POSITIVE_INFINITY) { + data[pos + 1] = b0x7c; + data[pos + 0] = b0x00; + } else { + data[pos + 1] = b0x7b; + data[pos + 0] = b0xff; + } + } else if (flt < -FLOAT_HALF_MAX) { + if (flt == Float.NEGATIVE_INFINITY) { + data[pos + 1] = b0xfc; + data[pos + 0] = b0x00; + } else { + data[pos + 1] = b0xfb; + data[pos + 0] = b0xff; + } + } else if (flt > 0f && flt < FLOAT_HALF_PREC) { + data[pos + 1] = b0x00; + data[pos + 0] = b0x01; + } else if (flt < 0f && flt > -FLOAT_HALF_PREC) { + data[pos + 1] = b0x80; + data[pos + 0] = b0x01; + } else { + int f = Float.floatToIntBits(flt); + + if (f == 0x7fc00000) + throw new UnsupportedOperationException("NaN to half conversion not supported!"); + + data[pos + 1] = (byte) (((f >> 24) & 0x80) + | ((((f & 0x7f800000) - 0x38000000) >> 21) & 0x7c) + | ((f >> 21) & 0x03)); + + data[pos + 0] = (byte) ((f >> 13) & 0xff); + } + } +} diff --git a/src/org/mapsforge/android/utils/GeometryUtils.java b/src/org/mapsforge/android/utils/GeometryUtils.java new file mode 100644 index 00000000..9a4ae50e --- /dev/null +++ b/src/org/mapsforge/android/utils/GeometryUtils.java @@ -0,0 +1,278 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.utils; + +/** + * + * + */ +public final class GeometryUtils { + /** + * Calculates the center of the minimum bounding rectangle for the given coordinates. + * + * @param coordinates + * the coordinates for which calculation should be done. + * @return the center coordinates of the minimum bounding rectangle. + */ + static float[] calculateCenterOfBoundingBox(float[] coordinates) { + float longitudeMin = coordinates[0]; + float longitudeMax = coordinates[0]; + float latitudeMax = coordinates[1]; + float latitudeMin = coordinates[1]; + + for (int i = 2; i < coordinates.length; i += 2) { + if (coordinates[i] < longitudeMin) { + longitudeMin = coordinates[i]; + } else if (coordinates[i] > longitudeMax) { + longitudeMax = coordinates[i]; + } + + if (coordinates[i + 1] < latitudeMin) { + latitudeMin = coordinates[i + 1]; + } else if (coordinates[i + 1] > latitudeMax) { + latitudeMax = coordinates[i + 1]; + } + } + + return new float[] { (longitudeMin + longitudeMax) / 2, (latitudeMax + latitudeMin) / 2 }; + } + + /** + * @param way + * the coordinates of the way. + * @return true if the given way is closed, false otherwise. + */ + static boolean isClosedWay(float[] way) { + return Float.compare(way[0], way[way.length - 2]) == 0 && Float.compare(way[1], way[way.length - 1]) == 0; + } + + private GeometryUtils() { + throw new IllegalStateException(); + } + + static boolean linesIntersect(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) { + // Return false if either of the lines have zero length + if (x1 == x2 && y1 == y2 || x3 == x4 && y3 == y4) { + return false; + } + // Fastest method, based on Franklin Antonio's + // "Faster Line Segment Intersection" topic "in Graphics Gems III" book + // (http://www.graphicsgems.org/) + double ax = x2 - x1; + double ay = y2 - y1; + double bx = x3 - x4; + double by = y3 - y4; + double cx = x1 - x3; + double cy = y1 - y3; + + double alphaNumerator = by * cx - bx * cy; + double commonDenominator = ay * bx - ax * by; + + if (commonDenominator > 0) { + if (alphaNumerator < 0 || alphaNumerator > commonDenominator) { + return false; + } + } else if (commonDenominator < 0) { + if (alphaNumerator > 0 || alphaNumerator < commonDenominator) { + return false; + } + } + double betaNumerator = ax * cy - ay * cx; + if (commonDenominator > 0) { + if (betaNumerator < 0 || betaNumerator > commonDenominator) { + return false; + } + } else if (commonDenominator < 0) { + if (betaNumerator > 0 || betaNumerator < commonDenominator) { + return false; + } + } + if (commonDenominator == 0) { + // This code wasn't in Franklin Antonio's method. It was added by Keith + // Woodward. + // The lines are parallel. + // Check if they're collinear. + double y3LessY1 = y3 - y1; + double collinearityTestForP3 = x1 * (y2 - y3) + x2 * (y3LessY1) + x3 * (y1 - y2); // see + // http://mathworld.wolfram.com/Collinear.html + // If p3 is collinear with p1 and p2 then p4 will also be collinear, + // since p1-p2 is parallel with p3-p4 + if (collinearityTestForP3 == 0) { + // The lines are collinear. Now check if they overlap. + if (x1 >= x3 && x1 <= x4 || x1 <= x3 && x1 >= x4 || x2 >= x3 && x2 <= x4 || x2 <= x3 && x2 >= x4 + || x3 >= x1 && x3 <= x2 || x3 <= x1 && x3 >= x2) { + if (y1 >= y3 && y1 <= y4 || y1 <= y3 && y1 >= y4 || y2 >= y3 && y2 <= y4 || y2 <= y3 && y2 >= y4 + || y3 >= y1 && y3 <= y2 || y3 <= y1 && y3 >= y2) { + return true; + } + } + } + return false; + } + return true; + } + + static boolean doesIntersect(double l1x1, double l1y1, double l1x2, double l1y2, double l2x1, double l2y1, + double l2x2, double l2y2) { + double denom = ((l2y2 - l2y1) * (l1x2 - l1x1)) - ((l2x2 - l2x1) * (l1y2 - l1y1)); + + if (denom == 0.0f) { + return false; + } + + double ua = (((l2x2 - l2x1) * (l1y1 - l2y1)) - ((l2y2 - l2y1) * (l1x1 - l2x1))) / denom; + double ub = (((l1x2 - l1x1) * (l1y1 - l2y1)) - ((l1y2 - l1y1) * (l1x1 - l2x1))) / denom; + + return ((ua >= 0.0d) && (ua <= 1.0d) && (ub >= 0.0d) && (ub <= 1.0d)); + } + + /** + * @param x1 + * ... + * @param y1 + * ... + * @param x2 + * ... + * @param y2 + * ... + * @param x3 + * ... + * @param y3 + * ... + * @param x4 + * ... + * @param y4 + * ... + * @return ... + */ + public static boolean lineIntersect(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) { + double denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); + if (denom == 0.0) { // Lines are parallel. + return false; + } + double ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom; + double ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom; + if (ua >= 0.0f && ua <= 1.0f && ub >= 0.0f && ub <= 1.0f) { + // Get the intersection point. + return true; + } + + return false; + } + + // private static final int OUT_LEFT = 1; + // private static final int OUT_TOP = 2; + // private static final int OUT_RIGHT = 4; + // private static final int OUT_BOTTOM = 8; + // + // + // private static int outcode(double x, double y) { + // /* + // * Note on casts to double below. If the arithmetic of + // * x+w or y+h is done in float, then some bits may be + // * lost if the binary exponents of x/y and w/h are not + // * similar. By converting to double before the addition + // * we force the addition to be carried out in double to + // * avoid rounding error in the comparison. + // * + // * See bug 4320890 for problems that this inaccuracy causes. + // */ + // int out = 0; + // if (this.width <= 0) { + // out |= OUT_LEFT | OUT_RIGHT; + // } else if (x < this.x) { + // out |= OUT_LEFT; + // } else if (x > this.x + (double) this.width) { + // out |= OUT_RIGHT; + // } + // if (this.height <= 0) { + // out |= OUT_TOP | OUT_BOTTOM; + // } else if (y < this.y) { + // out |= OUT_TOP; + // } else if (y > this.y + (double) this.height) { + // out |= OUT_BOTTOM; + // } + // return out; + // } + + // from http://shamimkhaliq.50megs.com/Java/lineclipper.htm + // private static int outCodes(Point P) + // { + // int Code = 0; + // + // if(P.y > yTop) Code += 1; /* code for above */ + // else if(P.y < yBottom) Code += 2; /* code for below */ + // + // if(P.x > xRight) Code += 4; /* code for right */ + // else if(P.x < xLeft) Code += 8; /* code for left */ + // + // return Code; + // } + // + // private static boolean rejectCheck(int outCode1, int outCode2) + // { + // if ((outCode1 & outCode2) != 0 ) return true; + // return(false); + // } + // + // + // private static boolean acceptCheck(int outCode1, int outCode2) + // { + // if ( (outCode1 == 0) && (outCode2 == 0) ) return(true); + // return(false); + // } + // + // static boolean CohenSutherland2DClipper(Point P0,Point P1) + // { + // int outCode0,outCode1; + // while(true) + // { + // outCode0 = outCodes(P0); + // outCode1 = outCodes(P1); + // if( rejectCheck(outCode0,outCode1) ) return(false); + // if( acceptCheck(outCode0,outCode1) ) return(true); + // if(outCode0 == 0) + // { + // double tempCoord; int tempCode; + // tempCoord = P0.x; P0.x= P1.x; P1.x = tempCoord; + // tempCoord = P0.y; P0.y= P1.y; P1.y = tempCoord; + // tempCode = outCode0; outCode0 = outCode1; outCode1 = tempCode; + // } + // if( (outCode0 & 1) != 0 ) + // { + // P0.x += (P1.x - P0.x)*(yTop - P0.y)/(P1.y - P0.y); + // P0.y = yTop; + // } + // else + // if( (outCode0 & 2) != 0 ) + // { + // P0.x += (P1.x - P0.x)*(yBottom - P0.y)/(P1.y - P0.y); + // P0.y = yBottom; + // } + // else + // if( (outCode0 & 4) != 0 ) + // { + // P0.y += (P1.y - P0.y)*(xRight - P0.x)/(P1.x - P0.x); + // P0.x = xRight; + // } + // else + // if( (outCode0 & 8) != 0 ) + // { + // P0.y += (P1.y - P0.y)*(xLeft - P0.x)/(P1.x - P0.x); + // P0.x = xLeft; + // } + // } + // } +} diff --git a/src/org/mapsforge/android/utils/GlConfigChooser.java b/src/org/mapsforge/android/utils/GlConfigChooser.java new file mode 100644 index 00000000..dd0d5d59 --- /dev/null +++ b/src/org/mapsforge/android/utils/GlConfigChooser.java @@ -0,0 +1,152 @@ +package org.mapsforge.android.utils; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; + +import android.opengl.GLSurfaceView; +import android.util.Log; + +/** + * + * + */ +public class GlConfigChooser implements GLSurfaceView.EGLConfigChooser { + static private final String TAG = "ConfigChooser"; + + /** + * + */ + public static int stencilSize = 0; + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + mValue = new int[1]; + + // Try to find a normal multisample configuration first. + int[] configSpec = { + EGL10.EGL_RED_SIZE, 5, + EGL10.EGL_GREEN_SIZE, 6, + EGL10.EGL_BLUE_SIZE, 5, + EGL10.EGL_ALPHA_SIZE, 0, + // EGL10.EGL_DEPTH_SIZE, 8, + // Requires that setEGLContextClientVersion(2) is called on the view. + EGL10.EGL_RENDERABLE_TYPE, 4 /* EGL_OPENGL_ES2_BIT */, + // EGL10.EGL_SAMPLE_BUFFERS, 1 /* true */, + // EGL10.EGL_SAMPLES, 2, + EGL10.EGL_STENCIL_SIZE, 8, + EGL10.EGL_NONE }; + + if (!egl.eglChooseConfig(display, configSpec, null, 0, mValue)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + int numConfigs = mValue[0]; + + if (numConfigs <= 0) { + stencilSize = 4; + + configSpec = new int[] { + EGL10.EGL_RED_SIZE, 5, + EGL10.EGL_GREEN_SIZE, 6, + EGL10.EGL_BLUE_SIZE, 5, + EGL10.EGL_ALPHA_SIZE, 0, + // EGL10.EGL_DEPTH_SIZE, 8, + EGL10.EGL_RENDERABLE_TYPE, 4 /* EGL_OPENGL_ES2_BIT */, + EGL10.EGL_STENCIL_SIZE, 4, + EGL10.EGL_NONE }; + + if (!egl.eglChooseConfig(display, configSpec, null, 0, mValue)) { + throw new IllegalArgumentException("3rd eglChooseConfig failed"); + } + numConfigs = mValue[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + } else { + stencilSize = 8; + } + + // Get all matching configurations. + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, mValue)) { + throw new IllegalArgumentException("data eglChooseConfig failed"); + } + + // CAUTION! eglChooseConfigs returns configs with higher bit depth + // first: Even though we asked for rgb565 configurations, rgb888 + // configurations are considered to be "better" and returned first. + // You need to explicitly filter the data returned by eglChooseConfig! + + // for (int i = 0; i < configs.length; ++i) { + // Log.i(TAG, printConfig(egl, display, configs[i])); + // } + // + // int index = -1; + // for (int i = 0; i < configs.length; ++i) { + // if (findConfigAttrib(egl, display, configs[i], EGL10.EGL_RED_SIZE, 0) == 8 && + // findConfigAttrib(egl, display, configs[i], EGL10.EGL_ALPHA_SIZE, 0) == 0) { + // index = i; + // break; + // } + // else if (findConfigAttrib(egl, display, configs[i], EGL10.EGL_RED_SIZE, 0) == 5 && + // findConfigAttrib(egl, display, configs[i], EGL10.EGL_ALPHA_SIZE, 0) == 0) { + // index = i; + // break; + // } + // } + // + // if (index == -1) { + // Log.w(TAG, "Did not find sane config, using first"); + // index = 0; + // } + int index = 0; + + Log.i(TAG, "using: " + printConfig(egl, display, configs[index])); + + EGLConfig config = configs.length > 0 ? configs[index] : null; + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + // from quake2android + private String printConfig(EGL10 egl, EGLDisplay display, + EGLConfig config) { + + int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); + int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); + + /* + * EGL_CONFIG_CAVEAT value + * #define EGL_NONE 0x3038 + * #define EGL_SLOW_CONFIG 0x3050 + * #define EGL_NON_CONFORMANT_CONFIG 0x3051 + */ + + return String.format("EGLConfig rgba=%d%d%d%d depth=%d stencil=%d", new Integer(r), new Integer(g), + new Integer(b), new Integer(a), new Integer(d), new Integer(s)) + + " native=" + + findConfigAttrib(egl, display, config, EGL10.EGL_NATIVE_RENDERABLE, 0) + + " buffer=" + + findConfigAttrib(egl, display, config, EGL10.EGL_BUFFER_SIZE, 0) + + String.format(" caveat=0x%04x", + new Integer(findConfigAttrib(egl, display, config, EGL10.EGL_CONFIG_CAVEAT, 0))); + + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) { + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + +} diff --git a/src/org/mapsforge/android/utils/GlUtils.java b/src/org/mapsforge/android/utils/GlUtils.java new file mode 100644 index 00000000..45e8d50b --- /dev/null +++ b/src/org/mapsforge/android/utils/GlUtils.java @@ -0,0 +1,128 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.utils; + +import android.graphics.Bitmap; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.util.Log; + +/** + * Utility functions + */ +public class GlUtils { + private static String TAG = "GlUtils"; + + /** + * @param bitmap + * ... + * @return gl identifier + */ + public static int loadTextures(Bitmap bitmap) { + + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + + int textureID = textures[0]; + // Log.i(TAG, "new texture " + textureID + " " + textureCnt++); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureID); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + + return textureID; + } + + /** + * @param shaderType + * shader type + * @param source + * shader code + * @return gl identifier + */ + public static int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + if (shader != 0) { + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + Log.e(TAG, "Could not compile shader " + shaderType + ":"); + Log.e(TAG, GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + } + return shader; + } + + /** + * @param vertexSource + * ... + * @param fragmentSource + * ... + * @return gl identifier + */ + public static int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + + int program = GLES20.glCreateProgram(); + if (program != 0) { + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG, "Could not link program: "); + Log.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + } + return program; + } + + /** + * @param op + * ... + */ + public static void checkGlError(String op) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + Log.e(TAG, op + ": glError " + error); + // throw new RuntimeException(op + ": glError " + error); + } + } +} diff --git a/src/org/mapsforge/android/utils/PausableThread.java b/src/org/mapsforge/android/utils/PausableThread.java new file mode 100644 index 00000000..780bde36 --- /dev/null +++ b/src/org/mapsforge/android/utils/PausableThread.java @@ -0,0 +1,142 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.android.utils; + +/** + * An abstract base class for threads which support pausing and resuming. + */ +public abstract class PausableThread extends Thread { + private boolean mPausing; + private boolean mShouldPause; + + /** + * Causes the current thread to wait until this thread is pausing. + */ + public final void awaitPausing() { + synchronized (this) { + while (!isInterrupted() && !isPausing()) { + try { + wait(100); + } catch (InterruptedException e) { + // restore the interrupted status + Thread.currentThread().interrupt(); + } + } + } + } + + /** + * @return true if this thread is currently pausing, false otherwise. + */ + public final synchronized boolean isPausing() { + return mPausing; + } + + /** + * The thread should stop its work temporarily. + */ + public final synchronized void pause() { + if (!mShouldPause) { + mShouldPause = true; + notify(); + } + } + + /** + * The paused thread should continue with its work. + */ + public final synchronized void proceed() { + if (mShouldPause) { + mShouldPause = false; + mPausing = false; + afterPause(); + notify(); + } + } + + @Override + public final void run() { + setName(getThreadName()); + setPriority(getThreadPriority()); + + while (!isInterrupted()) { + synchronized (this) { + while (!isInterrupted() && (mShouldPause || !hasWork())) { + try { + if (mShouldPause) { + mPausing = true; + } + wait(); + } catch (InterruptedException e) { + // restore the interrupted status + interrupt(); + } + } + } + + if (isInterrupted()) { + break; + } + + try { + doWork(); + } catch (InterruptedException e) { + // restore the interrupted status + interrupt(); + } + } + + afterRun(); + } + + /** + * Called once when this thread continues to work after a pause. The default implementation is empty. + */ + protected void afterPause() { + // do nothing + } + + /** + * Called once at the end of the {@link #run()} method. The default implementation is empty. + */ + protected void afterRun() { + // do nothing + } + + /** + * Called when this thread is not paused and should do its work. + * + * @throws InterruptedException + * if the thread has been interrupted. + */ + protected abstract void doWork() throws InterruptedException; + + /** + * @return the name of this thread. + */ + protected abstract String getThreadName(); + + /** + * @return the priority of this thread. The default value is {@link Thread#NORM_PRIORITY}. + */ + protected int getThreadPriority() { + return Thread.NORM_PRIORITY; + } + + /** + * @return true if this thread has some work to do, false otherwise. + */ + protected abstract boolean hasWork(); +} diff --git a/src/org/mapsforge/core/BoundingBox.java b/src/org/mapsforge/core/BoundingBox.java new file mode 100644 index 00000000..59a5b7fb --- /dev/null +++ b/src/org/mapsforge/core/BoundingBox.java @@ -0,0 +1,182 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.core; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +/** + * A BoundingBox represents an immutable set of two latitude and two longitude coordinates. + */ +public class BoundingBox implements Serializable { + /** + * Conversion factor from degrees to microdegrees. + */ + private static final double CONVERSION_FACTOR = 1000000d; + + private static final long serialVersionUID = 1L; + + private static boolean isBetween(int number, int min, int max) { + return min <= number && number <= max; + } + + /** + * The maximum latitude value of this BoundingBox in microdegrees (degrees * 10^6). + */ + public final int maxLatitudeE6; + + /** + * The maximum longitude value of this BoundingBox in microdegrees (degrees * 10^6). + */ + public final int maxLongitudeE6; + + /** + * The minimum latitude value of this BoundingBox in microdegrees (degrees * 10^6). + */ + public final int minLatitudeE6; + + /** + * The minimum longitude value of this BoundingBox in microdegrees (degrees * 10^6). + */ + public final int minLongitudeE6; + + /** + * The hash code of this object. + */ + private transient int hashCodeValue; + + /** + * @param minLatitudeE6 + * the minimum latitude in microdegrees (degrees * 10^6). + * @param minLongitudeE6 + * the minimum longitude in microdegrees (degrees * 10^6). + * @param maxLatitudeE6 + * the maximum latitude in microdegrees (degrees * 10^6). + * @param maxLongitudeE6 + * the maximum longitude in microdegrees (degrees * 10^6). + */ + public BoundingBox(int minLatitudeE6, int minLongitudeE6, int maxLatitudeE6, int maxLongitudeE6) { + this.minLatitudeE6 = minLatitudeE6; + this.minLongitudeE6 = minLongitudeE6; + this.maxLatitudeE6 = maxLatitudeE6; + this.maxLongitudeE6 = maxLongitudeE6; + this.hashCodeValue = calculateHashCode(); + } + + /** + * @param geoPoint + * the point whose coordinates should be checked. + * @return true if this BoundingBox contains the given GeoPoint, false otherwise. + */ + public boolean contains(GeoPoint geoPoint) { + return isBetween(geoPoint.latitudeE6, this.minLatitudeE6, this.maxLatitudeE6) + && isBetween(geoPoint.longitudeE6, this.minLongitudeE6, this.maxLongitudeE6); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof BoundingBox)) { + return false; + } + BoundingBox other = (BoundingBox) obj; + if (this.maxLatitudeE6 != other.maxLatitudeE6) { + return false; + } else if (this.maxLongitudeE6 != other.maxLongitudeE6) { + return false; + } else if (this.minLatitudeE6 != other.minLatitudeE6) { + return false; + } else if (this.minLongitudeE6 != other.minLongitudeE6) { + return false; + } + return true; + } + + /** + * @return the GeoPoint at the horizontal and vertical center of this BoundingBox. + */ + public GeoPoint getCenterPoint() { + int latitudeOffset = (this.maxLatitudeE6 - this.minLatitudeE6) / 2; + int longitudeOffset = (this.maxLongitudeE6 - this.minLongitudeE6) / 2; + return new GeoPoint(this.minLatitudeE6 + latitudeOffset, this.minLongitudeE6 + longitudeOffset); + } + + /** + * @return the maximum latitude value of this BoundingBox in degrees. + */ + public double getMaxLatitude() { + return this.maxLatitudeE6 / CONVERSION_FACTOR; + } + + /** + * @return the maximum longitude value of this BoundingBox in degrees. + */ + public double getMaxLongitude() { + return this.maxLongitudeE6 / CONVERSION_FACTOR; + } + + /** + * @return the minimum latitude value of this BoundingBox in degrees. + */ + public double getMinLatitude() { + return this.minLatitudeE6 / CONVERSION_FACTOR; + } + + /** + * @return the minimum longitude value of this BoundingBox in degrees. + */ + public double getMinLongitude() { + return this.minLongitudeE6 / CONVERSION_FACTOR; + } + + @Override + public int hashCode() { + return this.hashCodeValue; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("BoundingBox [minLatitudeE6="); + stringBuilder.append(this.minLatitudeE6); + stringBuilder.append(", minLongitudeE6="); + stringBuilder.append(this.minLongitudeE6); + stringBuilder.append(", maxLatitudeE6="); + stringBuilder.append(this.maxLatitudeE6); + stringBuilder.append(", maxLongitudeE6="); + stringBuilder.append(this.maxLongitudeE6); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + this.maxLatitudeE6; + result = 31 * result + this.maxLongitudeE6; + result = 31 * result + this.minLatitudeE6; + result = 31 * result + this.minLongitudeE6; + return result; + } + + private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { + objectInputStream.defaultReadObject(); + this.hashCodeValue = calculateHashCode(); + } +} diff --git a/src/org/mapsforge/core/GeoPoint.java b/src/org/mapsforge/core/GeoPoint.java new file mode 100644 index 00000000..51ec8d25 --- /dev/null +++ b/src/org/mapsforge/core/GeoPoint.java @@ -0,0 +1,147 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.core; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +/** + * A GeoPoint represents an immutable pair of latitude and longitude coordinates. + */ +public class GeoPoint implements Comparable, Serializable { + /** + * Conversion factor from degrees to microdegrees. + */ + private static final double CONVERSION_FACTOR = 1000000d; + + private static final long serialVersionUID = 1L; + + /** + * The latitude value of this GeoPoint in microdegrees (degrees * 10^6). + */ + public final int latitudeE6; + + /** + * The longitude value of this GeoPoint in microdegrees (degrees * 10^6). + */ + public final int longitudeE6; + + /** + * The hash code of this object. + */ + private transient int hashCodeValue; + + /** + * @param latitude + * the latitude in degrees, will be limited to the possible latitude range. + * @param longitude + * the longitude in degrees, will be limited to the possible longitude range. + */ + public GeoPoint(double latitude, double longitude) { + double limitLatitude = MercatorProjection.limitLatitude(latitude); + this.latitudeE6 = (int) (limitLatitude * CONVERSION_FACTOR); + + double limitLongitude = MercatorProjection.limitLongitude(longitude); + this.longitudeE6 = (int) (limitLongitude * CONVERSION_FACTOR); + + this.hashCodeValue = calculateHashCode(); + } + + /** + * @param latitudeE6 + * the latitude in microdegrees (degrees * 10^6), will be limited to the possible latitude range. + * @param longitudeE6 + * the longitude in microdegrees (degrees * 10^6), will be limited to the possible longitude range. + */ + public GeoPoint(int latitudeE6, int longitudeE6) { + this(latitudeE6 / CONVERSION_FACTOR, longitudeE6 / CONVERSION_FACTOR); + } + + @Override + public int compareTo(GeoPoint geoPoint) { + if (this.longitudeE6 > geoPoint.longitudeE6) { + return 1; + } else if (this.longitudeE6 < geoPoint.longitudeE6) { + return -1; + } else if (this.latitudeE6 > geoPoint.latitudeE6) { + return 1; + } else if (this.latitudeE6 < geoPoint.latitudeE6) { + return -1; + } + return 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof GeoPoint)) { + return false; + } + GeoPoint other = (GeoPoint) obj; + if (this.latitudeE6 != other.latitudeE6) { + return false; + } else if (this.longitudeE6 != other.longitudeE6) { + return false; + } + return true; + } + + /** + * @return the latitude value of this GeoPoint in degrees. + */ + public double getLatitude() { + return this.latitudeE6 / CONVERSION_FACTOR; + } + + /** + * @return the longitude value of this GeoPoint in degrees. + */ + public double getLongitude() { + return this.longitudeE6 / CONVERSION_FACTOR; + } + + @Override + public int hashCode() { + return this.hashCodeValue; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("GeoPoint [latitudeE6="); + stringBuilder.append(this.latitudeE6); + stringBuilder.append(", longitudeE6="); + stringBuilder.append(this.longitudeE6); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + this.latitudeE6; + result = 31 * result + this.longitudeE6; + return result; + } + + private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { + objectInputStream.defaultReadObject(); + this.hashCodeValue = calculateHashCode(); + } +} diff --git a/src/org/mapsforge/core/LRUCache.java b/src/org/mapsforge/core/LRUCache.java new file mode 100644 index 00000000..a1f06dbb --- /dev/null +++ b/src/org/mapsforge/core/LRUCache.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.core; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An LRUCache with a fixed size and an access-order policy. Old mappings are automatically removed from the cache when + * new mappings are added. This implementation uses an {@link LinkedHashMap} internally. + * + * @param + * the type of the map key, see {@link Map}. + * @param + * the type of the map value, see {@link Map}. + */ +public class LRUCache extends LinkedHashMap { + private static final float LOAD_FACTOR = 0.6f; + private static final long serialVersionUID = 1L; + + private static int calculateInitialCapacity(int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException("capacity must not be negative: " + capacity); + } + return (int) (capacity / LOAD_FACTOR) + 2; + } + + private final int capacity; + + /** + * @param capacity + * the maximum capacity of this cache. + * @throws IllegalArgumentException + * if the capacity is negative. + */ + public LRUCache(int capacity) { + super(calculateInitialCapacity(capacity), LOAD_FACTOR, true); + this.capacity = capacity; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > this.capacity; + } +} diff --git a/src/org/mapsforge/core/MapPosition.java b/src/org/mapsforge/core/MapPosition.java new file mode 100644 index 00000000..4be52eea --- /dev/null +++ b/src/org/mapsforge/core/MapPosition.java @@ -0,0 +1,112 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.core; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +/** + * A MapPosition represents an immutable pair of {@link GeoPoint} and zoom level. + */ +public class MapPosition implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The map position. + */ + public final GeoPoint geoPoint; + + /** + * The zoom level. + */ + public final byte zoomLevel; + + /** + * 1.0 - 2.0 scale of current zoomlevel + */ + public final float scale; + /** + * The hash code of this object. + */ + private transient int hashCodeValue; + + /** + * @param geoPoint + * the map position. + * @param zoomLevel + * the zoom level. + * @param scale + * ... + */ + public MapPosition(GeoPoint geoPoint, byte zoomLevel, float scale) { + this.geoPoint = geoPoint; + this.zoomLevel = zoomLevel; + this.scale = scale; + this.hashCodeValue = calculateHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof MapPosition)) { + return false; + } + MapPosition other = (MapPosition) obj; + if (this.geoPoint == null) { + if (other.geoPoint != null) { + return false; + } + } else if (!this.geoPoint.equals(other.geoPoint)) { + return false; + } + if (this.zoomLevel != other.zoomLevel) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return this.hashCodeValue; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("MapPosition [geoPoint="); + builder.append(this.geoPoint); + builder.append(", zoomLevel="); + builder.append(this.zoomLevel); + builder.append("]"); + return builder.toString(); + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + ((this.geoPoint == null) ? 0 : this.geoPoint.hashCode()); + result = 31 * result + this.zoomLevel; + return result; + } + + private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { + objectInputStream.defaultReadObject(); + this.hashCodeValue = calculateHashCode(); + } +} diff --git a/src/org/mapsforge/core/MercatorProjection.java b/src/org/mapsforge/core/MercatorProjection.java new file mode 100644 index 00000000..23babe09 --- /dev/null +++ b/src/org/mapsforge/core/MercatorProjection.java @@ -0,0 +1,213 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.core; + +/** + * An implementation of the spherical Mercator projection. + */ +public final class MercatorProjection { + /** + * The circumference of the earth at the equator in meters. + */ + public static final double EARTH_CIRCUMFERENCE = 40075016.686; + + /** + * Maximum possible latitude coordinate of the map. + */ + public static final double LATITUDE_MAX = 85.05112877980659; + + /** + * Minimum possible latitude coordinate of the map. + */ + public static final double LATITUDE_MIN = -LATITUDE_MAX; + + /** + * Maximum possible longitude coordinate of the map. + */ + public static final double LONGITUDE_MAX = 180; + + /** + * Minimum possible longitude coordinate of the map. + */ + public static final double LONGITUDE_MIN = -LONGITUDE_MAX; + + /** + * Calculates the distance on the ground that is represented by a single pixel on the map. + * + * @param latitude + * the latitude coordinate at which the resolution should be calculated. + * @param zoomLevel + * the zoom level at which the resolution should be calculated. + * @return the ground resolution at the given latitude and zoom level. + */ + public static double calculateGroundResolution(double latitude, byte zoomLevel) { + return Math.cos(latitude * (Math.PI / 180)) * EARTH_CIRCUMFERENCE / ((long) Tile.TILE_SIZE << zoomLevel); + } + + /** + * Converts a latitude coordinate (in degrees) to a pixel Y coordinate at a certain zoom level. + * + * @param latitude + * the latitude coordinate that should be converted. + * @param zoomLevel + * the zoom level at which the coordinate should be converted. + * @return the pixel Y coordinate of the latitude value. + */ + public static double latitudeToPixelY(double latitude, byte zoomLevel) { + double sinLatitude = Math.sin(latitude * (Math.PI / 180)); + return (0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI)) + * ((long) Tile.TILE_SIZE << zoomLevel); + } + + /** + * Converts a latitude coordinate (in degrees) to a tile Y number at a certain zoom level. + * + * @param latitude + * the latitude coordinate that should be converted. + * @param zoomLevel + * the zoom level at which the coordinate should be converted. + * @return the tile Y number of the latitude value. + */ + public static long latitudeToTileY(double latitude, byte zoomLevel) { + return pixelYToTileY(latitudeToPixelY(latitude, zoomLevel), zoomLevel); + } + + /** + * @param latitude + * the latitude value which should be checked. + * @return the given latitude value, limited to the possible latitude range. + */ + public static double limitLatitude(double latitude) { + return Math.max(Math.min(latitude, LATITUDE_MAX), LATITUDE_MIN); + } + + /** + * @param longitude + * the longitude value which should be checked. + * @return the given longitude value, limited to the possible longitude range. + */ + public static double limitLongitude(double longitude) { + return Math.max(Math.min(longitude, LONGITUDE_MAX), LONGITUDE_MIN); + } + + /** + * Converts a longitude coordinate (in degrees) to a pixel X coordinate at a certain zoom level. + * + * @param longitude + * the longitude coordinate that should be converted. + * @param zoomLevel + * the zoom level at which the coordinate should be converted. + * @return the pixel X coordinate of the longitude value. + */ + public static double longitudeToPixelX(double longitude, byte zoomLevel) { + return (longitude + 180) / 360 * ((long) Tile.TILE_SIZE << zoomLevel); + } + + /** + * Converts a longitude coordinate (in degrees) to the tile X number at a certain zoom level. + * + * @param longitude + * the longitude coordinate that should be converted. + * @param zoomLevel + * the zoom level at which the coordinate should be converted. + * @return the tile X number of the longitude value. + */ + public static long longitudeToTileX(double longitude, byte zoomLevel) { + return pixelXToTileX(longitudeToPixelX(longitude, zoomLevel), zoomLevel); + } + + /** + * Converts a pixel X coordinate at a certain zoom level to a longitude coordinate. + * + * @param pixelX + * the pixel X coordinate that should be converted. + * @param zoomLevel + * the zoom level at which the coordinate should be converted. + * @return the longitude value of the pixel X coordinate. + */ + public static double pixelXToLongitude(double pixelX, byte zoomLevel) { + return 360 * ((pixelX / ((long) Tile.TILE_SIZE << zoomLevel)) - 0.5); + } + + /** + * Converts a pixel X coordinate to the tile X number. + * + * @param pixelX + * the pixel X coordinate that should be converted. + * @param zoomLevel + * the zoom level at which the coordinate should be converted. + * @return the tile X number. + */ + public static long pixelXToTileX(double pixelX, byte zoomLevel) { + return (long) Math.min(Math.max(pixelX / Tile.TILE_SIZE, 0), Math.pow(2, zoomLevel) - 1); + } + + /** + * Converts a pixel Y coordinate at a certain zoom level to a latitude coordinate. + * + * @param pixelY + * the pixel Y coordinate that should be converted. + * @param zoomLevel + * the zoom level at which the coordinate should be converted. + * @return the latitude value of the pixel Y coordinate. + */ + public static double pixelYToLatitude(double pixelY, byte zoomLevel) { + double y = 0.5 - (pixelY / ((long) Tile.TILE_SIZE << zoomLevel)); + return 90 - 360 * Math.atan(Math.exp(-y * (2 * Math.PI))) / Math.PI; + } + + /** + * Converts a pixel Y coordinate to the tile Y number. + * + * @param pixelY + * the pixel Y coordinate that should be converted. + * @param zoomLevel + * the zoom level at which the coordinate should be converted. + * @return the tile Y number. + */ + public static long pixelYToTileY(double pixelY, byte zoomLevel) { + return (long) Math.min(Math.max(pixelY / Tile.TILE_SIZE, 0), Math.pow(2, zoomLevel) - 1); + } + + /** + * Converts a tile X number at a certain zoom level to a longitude coordinate. + * + * @param tileX + * the tile X number that should be converted. + * @param zoomLevel + * the zoom level at which the number should be converted. + * @return the longitude value of the tile X number. + */ + public static double tileXToLongitude(long tileX, byte zoomLevel) { + return pixelXToLongitude(tileX * Tile.TILE_SIZE, zoomLevel); + } + + /** + * Converts a tile Y number at a certain zoom level to a latitude coordinate. + * + * @param tileY + * the tile Y number that should be converted. + * @param zoomLevel + * the zoom level at which the number should be converted. + * @return the latitude value of the tile Y number. + */ + public static double tileYToLatitude(long tileY, byte zoomLevel) { + return pixelYToLatitude(tileY * Tile.TILE_SIZE, zoomLevel); + } + + private MercatorProjection() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/core/SphericalMercator.java b/src/org/mapsforge/core/SphericalMercator.java new file mode 100644 index 00000000..226464c0 --- /dev/null +++ b/src/org/mapsforge/core/SphericalMercator.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License along with + * this program. If not, see . + */ + +package org.mapsforge.core; + +/** + * + */ +public class SphericalMercator { + /** + * + */ + public static final String NAME = "SphericalMercator"; + + private static final double f900913 = 20037508.342789244; + private static final double f900913_2 = 20037508.342789244 * 2; + + /** + * @param lon + * ... + * @param z + * ... + * @param offset + * ... + * @return ... + */ + public static float sphericalMercatorToPixelX(float lon, long z, long offset) { + return (float) (((lon + f900913) / f900913_2) * z - offset); + } + + /** + * @param lat + * ... + * @param z + * ... + * @param offset + * ... + * @return ... + */ + public static float sphericalMercatorToPixelY(float lat, long z, long offset) { + return (float) (((lat + f900913) / f900913_2) * z + - (z - offset)); + } + + /** + * @param pixelY + * ... + * @param z + * ... + * @return ... + */ + public static double PixelYtoSphericalMercator(long pixelY, byte z) { + long half = (Tile.TILE_SIZE << z) >> 1; + return ((half - pixelY) / (double) half) * f900913; + } + + /** + * @param pixelX + * ... + * @param z + * ... + * @return ... + */ + public static double PixelXtoSphericalMercator(long pixelX, byte z) { + long half = (Tile.TILE_SIZE << z) >> 1; + return ((pixelX - half) / (double) half) * f900913; + } + + private static double radius = 6378137; + private static double D2R = Math.PI / 180; + private static double HALF_PI = Math.PI / 2; + + /** + * from http://pauldendulk.com/2011/04/projecting-from-wgs84-to.html + * + * @param lon + * ... + * @param lat + * ... + * @return ... + */ + public static float[] fromLonLat(double lon, double lat) + { + double lonRadians = (D2R * lon); + double latRadians = (D2R * lat); + + double x = radius * lonRadians; + double y = radius * Math.log(Math.tan(Math.PI * 0.25 + latRadians * 0.5)); + + float[] result = { (float) x, (float) y }; + return result; + } + + /** + * from http://pauldendulk.com/2011/04/projecting-from-wgs84-to.html + * + * @param x + * ... + * @param y + * ... + * @return ... + */ + public static float[] toLonLat(double x, double y) + { + double ts; + ts = Math.exp(-y / (radius)); + double latRadians = HALF_PI - 2 * Math.atan(ts); + + double lonRadians = x / (radius); + + double lon = (lonRadians / D2R); + double lat = (latRadians / D2R); + + float[] result = { (float) lon, (float) lat }; + return result; + } + +} diff --git a/src/org/mapsforge/core/Tag.java b/src/org/mapsforge/core/Tag.java new file mode 100644 index 00000000..c13014ca --- /dev/null +++ b/src/org/mapsforge/core/Tag.java @@ -0,0 +1,118 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.core; + +/** + * A tag represents an immutable key-value pair. + */ +public class Tag { + private static final char KEY_VALUE_SEPARATOR = '='; + /** + * The key of the house number OpenStreetMap tag. + */ + public static final String TAG_KEY_HOUSE_NUMBER = "addr:housenumber"; + + /** + * The key of the name OpenStreetMap tag. + */ + public static final String TAG_KEY_NAME = "name"; + + /** + * The key of the reference OpenStreetMap tag. + */ + public static final String TAG_KEY_REF = "ref"; + + /** + * The key of the elevation OpenStreetMap tag. + */ + public static final String TAG_KEY_ELE = "ele"; + + /** + * The key of this tag. + */ + public final String key; + + /** + * The value of this tag. + */ + public String value; + + private transient int hashCodeValue; + + /** + * @param tag + * the textual representation of the tag. + */ + + public Tag(String tag) { + int splitPosition = tag.indexOf(KEY_VALUE_SEPARATOR); + this.key = tag.substring(0, splitPosition).intern(); + this.value = tag.substring(splitPosition + 1).intern(); + this.hashCodeValue = calculateHashCode(); + } + + /** + * @param key + * the key of the tag. + * @param value + * the value of the tag. + */ + public Tag(String key, String value) { + this.key = (key == null ? null : key.intern()); + this.value = (value == null ? null : value.intern()); + this.hashCodeValue = calculateHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof Tag)) { + return false; + } + Tag other = (Tag) obj; + + if ((this.key != other.key) || (this.value != other.value)) + return false; + + return true; + } + + @Override + public int hashCode() { + return this.hashCodeValue; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Tag [key="); + stringBuilder.append(this.key); + stringBuilder.append(", value="); + stringBuilder.append(this.value); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + ((this.key == null) ? 0 : this.key.hashCode()); + result = 31 * result + ((this.value == null) ? 0 : this.value.hashCode()); + return result; + } +} diff --git a/src/org/mapsforge/core/Tile.java b/src/org/mapsforge/core/Tile.java new file mode 100644 index 00000000..b5b0a51c --- /dev/null +++ b/src/org/mapsforge/core/Tile.java @@ -0,0 +1,141 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.core; + + +/** + * A tile represents a rectangular part of the world map. All tiles can be identified by their X and Y number together + * with their zoom level. The actual area that a tile covers on a map depends on the underlying map projection. + */ +public class Tile { + /** + * Bytes per pixel required in a map tile bitmap. + */ + public static final byte TILE_BYTES_PER_PIXEL = 2; + + /** + * Width and height of a map tile in pixel. + */ + public static final int TILE_SIZE = 256; + + /** + * Size of a single uncompressed map tile bitmap in bytes. + */ + public static final int TILE_SIZE_IN_BYTES = TILE_SIZE * TILE_SIZE * TILE_BYTES_PER_PIXEL; + + /** + * The X number of this tile. + */ + public final long tileX; + + /** + * The Y number of this tile. + */ + public final long tileY; + + /** + * The Zoom level of this tile. + */ + public final byte zoomLevel; + + private transient int hashCodeValue; + /** + * the pixel X coordinate of the upper left corner of this tile. + */ + public final long pixelX; + /** + * the pixel Y coordinate of the upper left corner of this tile. + */ + public final long pixelY; + + /** + * @param tileX + * the X number of the tile. + * @param tileY + * the Y number of the tile. + * @param zoomLevel + * the zoom level of the tile. + */ + public Tile(long tileX, long tileY, byte zoomLevel) { + this.tileX = tileX; + this.tileY = tileY; + this.pixelX = this.tileX * TILE_SIZE; + this.pixelY = this.tileY * TILE_SIZE; + this.zoomLevel = zoomLevel; + this.hashCodeValue = calculateHashCode(); + + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof Tile)) { + return false; + } + Tile other = (Tile) obj; + if (this.tileX != other.tileX) { + return false; + } else if (this.tileY != other.tileY) { + return false; + } else if (this.zoomLevel != other.zoomLevel) { + return false; + } + return true; + } + + /** + * @return the pixel X coordinate of the upper left corner of this tile. + */ + // public long getPixelX() { + // return this.pixelX; + // } + + /** + * @return the pixel Y coordinate of the upper left corner of this tile. + */ + // public long getPixelY() { + // return this.pixelY; + // } + + @Override + public int hashCode() { + return this.hashCodeValue; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Tile [tileX="); + stringBuilder.append(this.tileX); + stringBuilder.append(", tileY="); + stringBuilder.append(this.tileY); + stringBuilder.append(", zoomLevel="); + stringBuilder.append(this.zoomLevel); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + (int) (this.tileX ^ (this.tileX >>> 32)); + result = 31 * result + (int) (this.tileY ^ (this.tileY >>> 32)); + result = 31 * result + this.zoomLevel; + return result; + } +} diff --git a/src/org/mapsforge/mapdatabase/FileOpenResult.java b/src/org/mapsforge/mapdatabase/FileOpenResult.java new file mode 100644 index 00000000..d3132427 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/FileOpenResult.java @@ -0,0 +1,75 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase; + + +/** + * A FileOpenResult is a simple DTO which is returned by IMapDatabase#openFile(File). + */ +public class FileOpenResult { + /** + * Singleton for a FileOpenResult instance with {@code success=true}. + */ + public static final FileOpenResult SUCCESS = new FileOpenResult(); + + private final String errorMessage; + private final boolean success; + + /** + * @param errorMessage + * a textual message describing the error, must not be null. + */ + public FileOpenResult(String errorMessage) { + if (errorMessage == null) { + throw new IllegalArgumentException("error message must not be null"); + } + + this.success = false; + this.errorMessage = errorMessage; + } + + /** + * + */ + public FileOpenResult() { + this.success = true; + this.errorMessage = null; + } + + /** + * @return a textual error description (might be null). + */ + public String getErrorMessage() { + return this.errorMessage; + } + + /** + * @return true if the file could be opened successfully, false otherwise. + */ + public boolean isSuccess() { + return this.success; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("FileOpenResult [success="); + stringBuilder.append(this.success); + stringBuilder.append(", errorMessage="); + stringBuilder.append(this.errorMessage); + stringBuilder.append("]"); + return stringBuilder.toString(); + } +} diff --git a/src/org/mapsforge/mapdatabase/IMapDatabase.java b/src/org/mapsforge/mapdatabase/IMapDatabase.java new file mode 100644 index 00000000..bc319241 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/IMapDatabase.java @@ -0,0 +1,72 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase; + +import java.io.File; + +import org.mapsforge.core.Tile; + +/** + * + * + */ +public interface IMapDatabase { + + /** + * Closes the map file and destroys all internal caches. Has no effect if no map file is currently opened. + */ + public abstract void closeFile(); + + /** + * Starts a database query with the given parameters. + * + * @param tile + * the tile to read. + * @param mapDatabaseCallback + * the callback which handles the extracted map elements. + */ + public abstract void executeQuery(Tile tile, IMapDatabaseCallback mapDatabaseCallback); + + /** + * @return the metadata for the current map file. + * @throws IllegalStateException + * if no map is currently opened. + */ + public abstract MapFileInfo getMapFileInfo(); + + /** + * @return true if a map file is currently opened, false otherwise. + */ + public abstract boolean hasOpenFile(); + + /** + * Opens the given map file, reads its header data and validates them. + * + * @param mapFile + * the map file. + * @return a FileOpenResult containing an error message in case of a failure. + * @throws IllegalArgumentException + * if the given map file is null. + */ + public abstract FileOpenResult openFile(File mapFile); + + /** + * @param position + * .... + * @return ... + */ + public abstract String readString(int position); + +} \ No newline at end of file diff --git a/src/org/mapsforge/mapdatabase/IMapDatabaseCallback.java b/src/org/mapsforge/mapdatabase/IMapDatabaseCallback.java new file mode 100644 index 00000000..ccbb44ff --- /dev/null +++ b/src/org/mapsforge/mapdatabase/IMapDatabaseCallback.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase; + +import org.mapsforge.core.Tag; +import org.mapsforge.mapdatabase.mapfile.MapDatabase; + +/** + * Callback methods which can be triggered from the {@link MapDatabase}. + */ +public interface IMapDatabaseCallback { + /** + * Renders a single point of interest node (POI). + * + * @param layer + * the layer of the node. + * @param latitude + * the latitude of the node. + * @param longitude + * the longitude of the node. + * @param tags + * the tags of the node. + */ + void renderPointOfInterest(byte layer, int latitude, int longitude, Tag[] tags); + + /** + * Renders water background for the current tile. + */ + void renderWaterBackground(); + + /** + * Renders a single way or area (closed way). + * + * @param layer + * the layer of the way. + * @param tags + * the tags of the way. + * @param wayNodes + * the geographical coordinates of the way nodes in the order longitude/latitude. + * @param wayLength + * length of way data in wayNodes + * @param changed + * tags have changed since last call (just an optional hint) + */ + void renderWay(byte layer, Tag[] tags, float[] wayNodes, int[] wayLength, boolean changed); +} diff --git a/src/org/mapsforge/mapdatabase/MapFileInfo.java b/src/org/mapsforge/mapdatabase/MapFileInfo.java new file mode 100644 index 00000000..6c10dc66 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/MapFileInfo.java @@ -0,0 +1,120 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase; + +import org.mapsforge.core.BoundingBox; +import org.mapsforge.core.GeoPoint; +import org.mapsforge.mapdatabase.mapfile.MapDatabase; + +/** + * Contains the immutable metadata of a map file. + * + * @see MapDatabase#getMapFileInfo() + */ +public class MapFileInfo { + /** + * The bounding box of the map file. + */ + public final BoundingBox boundingBox; + + /** + * The comment field of the map file (may be null). + */ + public final String comment; + + /** + * The created by field of the map file (may be null). + */ + public final String createdBy; + + /** + * The size of the map file, measured in bytes. + */ + public final long fileSize; + + /** + * The file version number of the map file. + */ + public final int fileVersion; + + /** + * The preferred language for names as defined in ISO 3166-1 (may be null). + */ + public final String languagePreference; + + /** + * The center point of the map file. + */ + public final GeoPoint mapCenter; + + /** + * The date of the map data in milliseconds since January 1, 1970. + */ + public final long mapDate; + + /** + * The name of the projection used in the map file. + */ + public final String projectionName; + + /** + * The map start position from the file header (may be null). + */ + public final GeoPoint startPosition; + + /** + * The map start zoom level from the file header (may be null). + */ + public final Byte startZoomLevel; + + /** + * @param bbox + * ... + * @param zoom + * ... + * @param start + * ... + * @param projection + * ... + * @param date + * ... + * @param size + * ... + * @param version + * ... + * @param language + * ... + * @param comment + * ... + * @param createdBy + * ... + */ + public MapFileInfo(BoundingBox bbox, Byte zoom, GeoPoint start, String projection, + long date, long size, int version, String language, String comment, String createdBy) { + + this.startZoomLevel = zoom; + this.startPosition = start; + this.projectionName = projection; + this.mapDate = date; + this.boundingBox = bbox; + this.mapCenter = bbox.getCenterPoint(); + this.languagePreference = language; + this.fileSize = size; + this.fileVersion = version; + + this.comment = comment; + this.createdBy = createdBy; + } +} diff --git a/src/org/mapsforge/mapdatabase/json/MapDatabase.java b/src/org/mapsforge/mapdatabase/json/MapDatabase.java new file mode 100644 index 00000000..55622a6b --- /dev/null +++ b/src/org/mapsforge/mapdatabase/json/MapDatabase.java @@ -0,0 +1,145 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.json; + +import java.io.File; + +import org.mapsforge.core.BoundingBox; +import org.mapsforge.core.MercatorProjection; +import org.mapsforge.core.Tag; +import org.mapsforge.core.Tile; +import org.mapsforge.mapdatabase.FileOpenResult; +import org.mapsforge.mapdatabase.IMapDatabase; +import org.mapsforge.mapdatabase.IMapDatabaseCallback; +import org.mapsforge.mapdatabase.MapFileInfo; + +/** + * + * + */ +public class MapDatabase implements IMapDatabase { + + private float[] mCoords = new float[20]; + private int[] mIndex = new int[1]; + // private Tag[] mTags = { new Tag("boundary", "administrative"), new Tag("admin_level", "2") }; + private Tag[] mTags = { new Tag("building", "yes") }; + private final MapFileInfo mMapInfo = + new MapFileInfo(new BoundingBox(-180, -90, 180, 90), + new Byte((byte) 0), null, "Mercator", 0, 0, 0, "de", "yo!", "by me"); + + private boolean mOpenFile = false; + + private static double radius = 6378137; + private static double D2R = Math.PI / 180; + + // private static double HALF_PI = Math.PI / 2; + + @Override + public void executeQuery(Tile tile, IMapDatabaseCallback mapDatabaseCallback) { + + long cx = tile.pixelX + (Tile.TILE_SIZE >> 1); + long cy = tile.pixelY + (Tile.TILE_SIZE >> 1); + // float lon1 = (float) MercatorProjection.pixelXToLongitude(cx - 100, tile.zoomLevel) * 1000000; + // float lon2 = (float) MercatorProjection.pixelXToLongitude(cx + 100, tile.zoomLevel) * 1000000; + // float lat1 = (float) MercatorProjection.pixelYToLatitude(cy - 100, tile.zoomLevel) * 1000000; + // float lat2 = (float) MercatorProjection.pixelYToLatitude(cy + 100, tile.zoomLevel) * 1000000; + + float lon1 = (float) MercatorProjection.pixelXToLongitude(cx - 100, tile.zoomLevel); + float lon2 = (float) MercatorProjection.pixelXToLongitude(cx + 100, tile.zoomLevel); + float lat1 = (float) MercatorProjection.pixelYToLatitude(cy - 100, tile.zoomLevel); + float lat2 = (float) MercatorProjection.pixelYToLatitude(cy + 100, tile.zoomLevel); + + double lonRadians = (D2R * lon1); + double latRadians = (D2R * lat1); + + // spherical mercator projection + lon1 = (float) (radius * lonRadians); + lat1 = (float) (radius * Math.log(Math.tan(Math.PI * 0.25 + latRadians * 0.5))); + + lonRadians = (D2R * lon2); + latRadians = (D2R * lat2); + + lon2 = (float) (radius * lonRadians); + lat2 = (float) (radius * Math.log(Math.tan(Math.PI * 0.25 + latRadians * 0.5))); + + mCoords[0] = lon1; + mCoords[1] = lat1; + + mCoords[2] = lon2; + mCoords[3] = lat1; + + mCoords[4] = lon2; + mCoords[5] = lat2; + + mCoords[6] = lon1; + mCoords[7] = lat2; + + mCoords[8] = lon1; + mCoords[9] = lat1; + + mIndex[0] = 10; + + // lon1 = (float) MercatorProjection.pixelXToLongitude(cx - 80, tile.zoomLevel) * 1000000; + // lon2 = (float) MercatorProjection.pixelXToLongitude(cx + 80, tile.zoomLevel) * 1000000; + // lat1 = (float) MercatorProjection.pixelYToLatitude(cy - 80, tile.zoomLevel) * 1000000; + // lat2 = (float) MercatorProjection.pixelYToLatitude(cy + 80, tile.zoomLevel) * 1000000; + // + // mCoords[10] = lon1; + // mCoords[11] = lat1; + // + // mCoords[12] = lon2; + // mCoords[13] = lat1; + // + // mCoords[14] = lon2; + // mCoords[15] = lat2; + // + // mCoords[16] = lon1; + // mCoords[17] = lat2; + // + // mCoords[18] = lon1; + // mCoords[19] = lat1; + // + // mIndex[1] = 10; + + mapDatabaseCallback.renderWay((byte) 0, mTags, mCoords, mIndex, true); + } + + @Override + public MapFileInfo getMapFileInfo() { + return mMapInfo; + } + + @Override + public boolean hasOpenFile() { + return mOpenFile; + } + + @Override + public FileOpenResult openFile(File mapFile) { + mOpenFile = true; + return new FileOpenResult(); + } + + @Override + public void closeFile() { + mOpenFile = false; + } + + @Override + public String readString(int position) { + return null; + } + +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/Deserializer.java b/src/org/mapsforge/mapdatabase/mapfile/Deserializer.java new file mode 100644 index 00000000..18a0565c --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/Deserializer.java @@ -0,0 +1,91 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile; + +/** + * This utility class contains methods to convert byte arrays to numbers. + */ +final class Deserializer { + /** + * Converts five bytes of a byte array to an unsigned long. + *

+ * The byte order is big-endian. + * + * @param buffer + * the byte array. + * @param offset + * the offset in the array. + * @return the long value. + */ + static long getFiveBytesLong(byte[] buffer, int offset) { + return (buffer[offset] & 0xffL) << 32 | (buffer[offset + 1] & 0xffL) << 24 | (buffer[offset + 2] & 0xffL) << 16 + | (buffer[offset + 3] & 0xffL) << 8 | (buffer[offset + 4] & 0xffL); + } + + /** + * Converts four bytes of a byte array to a signed int. + *

+ * The byte order is big-endian. + * + * @param buffer + * the byte array. + * @param offset + * the offset in the array. + * @return the int value. + */ + static int getInt(byte[] buffer, int offset) { + return buffer[offset] << 24 | (buffer[offset + 1] & 0xff) << 16 | (buffer[offset + 2] & 0xff) << 8 + | (buffer[offset + 3] & 0xff); + } + + /** + * Converts eight bytes of a byte array to a signed long. + *

+ * The byte order is big-endian. + * + * @param buffer + * the byte array. + * @param offset + * the offset in the array. + * @return the long value. + */ + static long getLong(byte[] buffer, int offset) { + return (buffer[offset] & 0xffL) << 56 | (buffer[offset + 1] & 0xffL) << 48 | (buffer[offset + 2] & 0xffL) << 40 + | (buffer[offset + 3] & 0xffL) << 32 | (buffer[offset + 4] & 0xffL) << 24 + | (buffer[offset + 5] & 0xffL) << 16 | (buffer[offset + 6] & 0xffL) << 8 | (buffer[offset + 7] & 0xffL); + } + + /** + * Converts two bytes of a byte array to a signed int. + *

+ * The byte order is big-endian. + * + * @param buffer + * the byte array. + * @param offset + * the offset in the array. + * @return the int value. + */ + static int getShort(byte[] buffer, int offset) { + return buffer[offset] << 8 | (buffer[offset + 1] & 0xff); + } + + /** + * Private constructor to prevent instantiation from other classes. + */ + private Deserializer() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/IndexCache.java b/src/org/mapsforge/mapdatabase/mapfile/IndexCache.java new file mode 100644 index 00000000..1c3dd84d --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/IndexCache.java @@ -0,0 +1,119 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.mapsforge.core.LRUCache; +import org.mapsforge.mapdatabase.mapfile.header.SubFileParameter; + +/** + * A cache for database index blocks with a fixed size and LRU policy. + */ +class IndexCache { + /** + * Number of index entries that one index block consists of. + */ + private static final int INDEX_ENTRIES_PER_BLOCK = 128; + + private static final Logger LOG = Logger.getLogger(IndexCache.class.getName()); + + /** + * Maximum size in bytes of one index block. + */ + private static final int SIZE_OF_INDEX_BLOCK = INDEX_ENTRIES_PER_BLOCK * SubFileParameter.BYTES_PER_INDEX_ENTRY; + + private final Map map; + private final RandomAccessFile randomAccessFile; + + /** + * @param randomAccessFile + * the map file from which the index should be read and cached. + * @param capacity + * the maximum number of entries in the cache. + * @throws IllegalArgumentException + * if the capacity is negative. + */ + IndexCache(RandomAccessFile randomAccessFile, int capacity) { + this.randomAccessFile = randomAccessFile; + this.map = new LRUCache(capacity); + } + + /** + * Destroy the cache at the end of its lifetime. + */ + void destroy() { + this.map.clear(); + } + + /** + * Returns the index entry of a block in the given map file. If the required index entry is not cached, it will be + * read from the map file index and put in the cache. + * + * @param subFileParameter + * the parameters of the map file for which the index entry is needed. + * @param blockNumber + * the number of the block in the map file. + * @return the index entry or -1 if the block number is invalid. + */ + long getIndexEntry(SubFileParameter subFileParameter, long blockNumber) { + try { + // check if the block number is out of bounds + if (blockNumber >= subFileParameter.numberOfBlocks) { + return -1; + } + + // calculate the index block number + long indexBlockNumber = blockNumber / INDEX_ENTRIES_PER_BLOCK; + + // create the cache entry key for this request + IndexCacheEntryKey indexCacheEntryKey = new IndexCacheEntryKey(subFileParameter, indexBlockNumber); + + // check for cached index block + byte[] indexBlock = this.map.get(indexCacheEntryKey); + if (indexBlock == null) { + // cache miss, seek to the correct index block in the file and read it + long indexBlockPosition = subFileParameter.indexStartAddress + indexBlockNumber * SIZE_OF_INDEX_BLOCK; + + int remainingIndexSize = (int) (subFileParameter.indexEndAddress - indexBlockPosition); + int indexBlockSize = Math.min(SIZE_OF_INDEX_BLOCK, remainingIndexSize); + indexBlock = new byte[indexBlockSize]; + + this.randomAccessFile.seek(indexBlockPosition); + if (this.randomAccessFile.read(indexBlock, 0, indexBlockSize) != indexBlockSize) { + LOG.warning("reading the current index block has failed"); + return -1; + } + + // put the index block in the map + this.map.put(indexCacheEntryKey, indexBlock); + } + + // calculate the address of the index entry inside the index block + long indexEntryInBlock = blockNumber % INDEX_ENTRIES_PER_BLOCK; + int addressInIndexBlock = (int) (indexEntryInBlock * SubFileParameter.BYTES_PER_INDEX_ENTRY); + + // return the real index entry + return Deserializer.getFiveBytesLong(indexBlock, addressInIndexBlock); + } catch (IOException e) { + LOG.log(Level.SEVERE, null, e); + return -1; + } + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/IndexCacheEntryKey.java b/src/org/mapsforge/mapdatabase/mapfile/IndexCacheEntryKey.java new file mode 100644 index 00000000..937e056d --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/IndexCacheEntryKey.java @@ -0,0 +1,73 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile; + +import org.mapsforge.mapdatabase.mapfile.header.SubFileParameter; + +/** + * An immutable container class which is the key for the index cache. + */ +class IndexCacheEntryKey { + private final int hashCodeValue; + private final long indexBlockNumber; + private final SubFileParameter subFileParameter; + + /** + * Creates an immutable key to be stored in a map. + * + * @param subFileParameter + * the parameters of the map file. + * @param indexBlockNumber + * the number of the index block. + */ + IndexCacheEntryKey(SubFileParameter subFileParameter, long indexBlockNumber) { + this.subFileParameter = subFileParameter; + this.indexBlockNumber = indexBlockNumber; + this.hashCodeValue = calculateHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof IndexCacheEntryKey)) { + return false; + } + IndexCacheEntryKey other = (IndexCacheEntryKey) obj; + if (this.subFileParameter == null && other.subFileParameter != null) { + return false; + } else if (this.subFileParameter != null && !this.subFileParameter.equals(other.subFileParameter)) { + return false; + } else if (this.indexBlockNumber != other.indexBlockNumber) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return this.hashCodeValue; + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + ((this.subFileParameter == null) ? 0 : this.subFileParameter.hashCode()); + result = 31 * result + (int) (this.indexBlockNumber ^ (this.indexBlockNumber >>> 32)); + return result; + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/MapDatabase.java b/src/org/mapsforge/mapdatabase/mapfile/MapDatabase.java new file mode 100644 index 00000000..c9db8e9b --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/MapDatabase.java @@ -0,0 +1,949 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.mapsforge.core.MercatorProjection; +import org.mapsforge.core.Tag; +import org.mapsforge.core.Tile; +import org.mapsforge.mapdatabase.FileOpenResult; +import org.mapsforge.mapdatabase.IMapDatabase; +import org.mapsforge.mapdatabase.IMapDatabaseCallback; +import org.mapsforge.mapdatabase.mapfile.header.MapFileHeader; +import org.mapsforge.mapdatabase.mapfile.header.MapFileInfo; +import org.mapsforge.mapdatabase.mapfile.header.SubFileParameter; + +/** + * A class for reading binary map files. + *

+ * This class is not thread-safe. Each thread should use its own instance. + * + * @see Specification + */ +public class MapDatabase implements IMapDatabase { + /** + * Bitmask to extract the block offset from an index entry. + */ + private static final long BITMASK_INDEX_OFFSET = 0x7FFFFFFFFFL; + + /** + * Bitmask to extract the water information from an index entry. + */ + private static final long BITMASK_INDEX_WATER = 0x8000000000L; + + /** + * Debug message prefix for the block signature. + */ + private static final String DEBUG_SIGNATURE_BLOCK = "block signature: "; + + /** + * Debug message prefix for the POI signature. + */ + // private static final String DEBUG_SIGNATURE_POI = "POI signature: "; + + /** + * Debug message prefix for the way signature. + */ + private static final String DEBUG_SIGNATURE_WAY = "way signature: "; + + /** + * Amount of cache blocks that the index cache should store. + */ + private static final int INDEX_CACHE_SIZE = 64; + + /** + * Error message for an invalid first way offset. + */ + private static final String INVALID_FIRST_WAY_OFFSET = "invalid first way offset: "; + + private static final Logger LOG = Logger.getLogger(MapDatabase.class.getName()); + + /** + * Maximum way nodes sequence length which is considered as valid. + */ + private static final int MAXIMUM_WAY_NODES_SEQUENCE_LENGTH = 8192; + + /** + * Maximum number of map objects in the zoom table which is considered as valid. + */ + private static final int MAXIMUM_ZOOM_TABLE_OBJECTS = 65536; + + /** + * Bitmask for the optional POI feature "elevation". + */ + private static final int POI_FEATURE_ELEVATION = 0x20; + + /** + * Bitmask for the optional POI feature "house number". + */ + private static final int POI_FEATURE_HOUSE_NUMBER = 0x40; + + /** + * Bitmask for the optional POI feature "name". + */ + private static final int POI_FEATURE_NAME = 0x80; + + /** + * Bitmask for the POI layer. + */ + private static final int POI_LAYER_BITMASK = 0xf0; + + /** + * Bit shift for calculating the POI layer. + */ + private static final int POI_LAYER_SHIFT = 4; + + /** + * Bitmask for the number of POI tags. + */ + private static final int POI_NUMBER_OF_TAGS_BITMASK = 0x0f; + + private static final String READ_ONLY_MODE = "r"; + + /** + * Length of the debug signature at the beginning of each block. + */ + private static final byte SIGNATURE_LENGTH_BLOCK = 32; + + /** + * Length of the debug signature at the beginning of each POI. + */ + private static final byte SIGNATURE_LENGTH_POI = 32; + + /** + * Length of the debug signature at the beginning of each way. + */ + private static final byte SIGNATURE_LENGTH_WAY = 32; + + /** + * Bitmask for the optional way data blocks byte. + */ + private static final int WAY_FEATURE_DATA_BLOCKS_BYTE = 0x08; + + /** + * Bitmask for the optional way double delta encoding. + */ + private static final int WAY_FEATURE_DOUBLE_DELTA_ENCODING = 0x04; + + /** + * Bitmask for the optional way feature "house number". + */ + private static final int WAY_FEATURE_HOUSE_NUMBER = 0x40; + + /** + * Bitmask for the optional way feature "label position". + */ + private static final int WAY_FEATURE_LABEL_POSITION = 0x10; + + /** + * Bitmask for the optional way feature "name". + */ + private static final int WAY_FEATURE_NAME = 0x80; + + /** + * Bitmask for the optional way feature "reference". + */ + private static final int WAY_FEATURE_REF = 0x20; + + /** + * Bitmask for the way layer. + */ + private static final int WAY_LAYER_BITMASK = 0xf0; + + /** + * Bit shift for calculating the way layer. + */ + private static final int WAY_LAYER_SHIFT = 4; + + /** + * Bitmask for the number of way tags. + */ + private static final int WAY_NUMBER_OF_TAGS_BITMASK = 0x0f; + + private IndexCache mDatabaseIndexCache; + private long mFileSize; + private boolean mDebugFile; + private RandomAccessFile mInputFile; + private MapFileHeader mMapFileHeader; + private ReadBuffer mReadBuffer; + private String mSignatureBlock; + private String mSignaturePoi; + private String mSignatureWay; + private int mTileLatitude; + private int mTileLongitude; + private int[] mIntBuffer; + + private float[] mWayNodes = new float[100000]; + private int mWayNodePosition; + + /* + * (non-Javadoc) + * @see org.mapsforge.map.reader.IMapDatabase#closeFile() + */ + @Override + public void closeFile() { + try { + mMapFileHeader = null; + + if (mDatabaseIndexCache != null) { + mDatabaseIndexCache.destroy(); + mDatabaseIndexCache = null; + } + + if (mInputFile != null) { + mInputFile.close(); + mInputFile = null; + } + + mReadBuffer = null; + } catch (IOException e) { + LOG.log(Level.SEVERE, null, e); + } + } + + private int minLat, minLon; + + /* + * (non-Javadoc) + * @see org.mapsforge.map.reader.IMapDatabase#executeQuery(org.mapsforge.core.Tile, + * org.mapsforge.map.reader.MapDatabaseCallback) + */ + @Override + public void executeQuery(Tile tile, IMapDatabaseCallback mapDatabaseCallback) { + if (mIntBuffer == null) + mIntBuffer = new int[MAXIMUM_WAY_NODES_SEQUENCE_LENGTH * 2]; + + mWayNodePosition = 0; + + // if (tile.zoomLevel < 10) { + // // reduce small nodes with distance smaller min pixel + // int min = 1; + // long cx = tile.getPixelX() + (Tile.TILE_SIZE >> 1); + // long cy = tile.getPixelY() + (Tile.TILE_SIZE >> 1); + // double l1 = MercatorProjection.pixelXToLongitude(cx, tile.zoomLevel); + // double l2 = MercatorProjection.pixelXToLongitude(cx + min, tile.zoomLevel); + // minLon = (int) Math.abs((l1 * 1000000.0) - (l2 * 1000000.0)); + // l1 = MercatorProjection.pixelYToLatitude(cy, tile.zoomLevel); + // l2 = MercatorProjection.pixelYToLatitude(cy + min, tile.zoomLevel); + // minLat = (int) Math.abs((l1 * 1000000.0) - (l2 * 1000000.0)); + // } else { + minLat = 0; + minLon = 0; + // } + + try { + prepareExecution(); + QueryParameters queryParameters = new QueryParameters(); + queryParameters.queryZoomLevel = mMapFileHeader.getQueryZoomLevel(tile.zoomLevel); + // get and check the sub-file for the query zoom level + SubFileParameter subFileParameter = mMapFileHeader.getSubFileParameter(queryParameters.queryZoomLevel); + if (subFileParameter == null) { + LOG.warning("no sub-file for zoom level: " + queryParameters.queryZoomLevel); + return; + } + + QueryCalculations.calculateBaseTiles(queryParameters, tile, subFileParameter); + QueryCalculations.calculateBlocks(queryParameters, subFileParameter); + processBlocks(mapDatabaseCallback, queryParameters, subFileParameter); + } catch (IOException e) { + LOG.log(Level.SEVERE, null, e); + } + } + + /* + * (non-Javadoc) + * @see org.mapsforge.map.reader.IMapDatabase#getMapFileInfo() + */ + @Override + public MapFileInfo getMapFileInfo() { + if (mMapFileHeader == null) { + throw new IllegalStateException("no map file is currently opened"); + } + return mMapFileHeader.getMapFileInfo(); + } + + /* + * (non-Javadoc) + * @see org.mapsforge.map.reader.IMapDatabase#hasOpenFile() + */ + @Override + public boolean hasOpenFile() { + return mInputFile != null; + } + + /* + * (non-Javadoc) + * @see org.mapsforge.map.reader.IMapDatabase#openFile(java.io.File) + */ + @Override + public FileOpenResult openFile(File mapFile) { + try { + if (mapFile == null) { + throw new IllegalArgumentException("mapFile must not be null"); + } + + // make sure to close any previously opened file first + closeFile(); + + // check if the file exists and is readable + if (!mapFile.exists()) { + return new FileOpenResult("file does not exist: " + mapFile); + } else if (!mapFile.isFile()) { + return new FileOpenResult("not a file: " + mapFile); + } else if (!mapFile.canRead()) { + return new FileOpenResult("cannot read file: " + mapFile); + } + + // open the file in read only mode + mInputFile = new RandomAccessFile(mapFile, READ_ONLY_MODE); + mFileSize = mInputFile.length(); + mReadBuffer = new ReadBuffer(mInputFile); + + mMapFileHeader = new MapFileHeader(); + FileOpenResult fileOpenResult = mMapFileHeader.readHeader(mReadBuffer, mFileSize); + if (!fileOpenResult.isSuccess()) { + closeFile(); + return fileOpenResult; + } + + return FileOpenResult.SUCCESS; + } catch (IOException e) { + LOG.log(Level.SEVERE, null, e); + // make sure that the file is closed + closeFile(); + return new FileOpenResult(e.getMessage()); + } + } + + /** + * Logs the debug signatures of the current way and block. + */ + private void logDebugSignatures() { + if (mDebugFile) { + LOG.warning(DEBUG_SIGNATURE_WAY + mSignatureWay); + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + } + } + + private void prepareExecution() { + if (mDatabaseIndexCache == null) { + mDatabaseIndexCache = new IndexCache(mInputFile, INDEX_CACHE_SIZE); + } + } + + /** + * Processes a single block and executes the callback functions on all map elements. + * + * @param queryParameters + * the parameters of the current query. + * @param subFileParameter + * the parameters of the current map file. + * @param mapDatabaseCallback + * the callback which handles the extracted map elements. + */ + private void processBlock(QueryParameters queryParameters, SubFileParameter subFileParameter, + IMapDatabaseCallback mapDatabaseCallback) { + if (!processBlockSignature()) { + return; + } + + int[][] zoomTable = readZoomTable(subFileParameter); + if (zoomTable == null) { + return; + } + int zoomTableRow = queryParameters.queryZoomLevel - subFileParameter.zoomLevelMin; + int poisOnQueryZoomLevel = zoomTable[zoomTableRow][0]; + int waysOnQueryZoomLevel = zoomTable[zoomTableRow][1]; + + // get the relative offset to the first stored way in the block + int firstWayOffset = mReadBuffer.readUnsignedInt(); + if (firstWayOffset < 0) { + LOG.warning(INVALID_FIRST_WAY_OFFSET + firstWayOffset); + if (mDebugFile) { + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + } + return; + } + + // add the current buffer position to the relative first way offset + firstWayOffset += mReadBuffer.getBufferPosition(); + if (firstWayOffset > mReadBuffer.getBufferSize()) { + LOG.warning(INVALID_FIRST_WAY_OFFSET + firstWayOffset); + if (mDebugFile) { + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + } + return; + } + + if (!processPOIs(mapDatabaseCallback, poisOnQueryZoomLevel)) { + return; + } + + // finished reading POIs, check if the current buffer position is valid + if (mReadBuffer.getBufferPosition() > firstWayOffset) { + LOG.warning("invalid buffer position: " + mReadBuffer.getBufferPosition()); + if (mDebugFile) { + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + } + return; + } + + // move the pointer to the first way + mReadBuffer.setBufferPosition(firstWayOffset); + if (!processWays(queryParameters, mapDatabaseCallback, waysOnQueryZoomLevel)) { + return; + } + + } + + private void processBlocks(IMapDatabaseCallback mapDatabaseCallback, QueryParameters queryParameters, + SubFileParameter subFileParameter) throws IOException { + boolean queryIsWater = true; + // boolean queryReadWaterInfo = false; + + // read and process all blocks from top to bottom and from left to right + for (long row = queryParameters.fromBlockY; row <= queryParameters.toBlockY; ++row) { + for (long column = queryParameters.fromBlockX; column <= queryParameters.toBlockX; ++column) { + + // calculate the actual block number of the needed block in the file + long blockNumber = row * subFileParameter.blocksWidth + column; + + // get the current index entry + long currentBlockIndexEntry = mDatabaseIndexCache.getIndexEntry(subFileParameter, blockNumber); + + // check if the current query would still return a water tile + if (queryIsWater) { + // check the water flag of the current block in its index entry + queryIsWater &= (currentBlockIndexEntry & BITMASK_INDEX_WATER) != 0; + // queryReadWaterInfo = true; + } + + // get and check the current block pointer + long currentBlockPointer = currentBlockIndexEntry & BITMASK_INDEX_OFFSET; + if (currentBlockPointer < 1 || currentBlockPointer > subFileParameter.subFileSize) { + LOG.warning("invalid current block pointer: " + currentBlockPointer); + LOG.warning("subFileSize: " + subFileParameter.subFileSize); + return; + } + + long nextBlockPointer; + // check if the current block is the last block in the file + if (blockNumber + 1 == subFileParameter.numberOfBlocks) { + // set the next block pointer to the end of the file + nextBlockPointer = subFileParameter.subFileSize; + } else { + // get and check the next block pointer + nextBlockPointer = mDatabaseIndexCache.getIndexEntry(subFileParameter, blockNumber + 1) + & BITMASK_INDEX_OFFSET; + if (nextBlockPointer < 1 || nextBlockPointer > subFileParameter.subFileSize) { + LOG.warning("invalid next block pointer: " + nextBlockPointer); + LOG.warning("sub-file size: " + subFileParameter.subFileSize); + return; + } + } + + // calculate the size of the current block + int currentBlockSize = (int) (nextBlockPointer - currentBlockPointer); + if (currentBlockSize < 0) { + LOG.warning("current block size must not be negative: " + currentBlockSize); + return; + } else if (currentBlockSize == 0) { + // the current block is empty, continue with the next block + continue; + } else if (currentBlockSize > ReadBuffer.MAXIMUM_BUFFER_SIZE) { + // the current block is too large, continue with the next block + LOG.warning("current block size too large: " + currentBlockSize); + continue; + } else if (currentBlockPointer + currentBlockSize > mFileSize) { + LOG.warning("current block largher than file size: " + currentBlockSize); + return; + } + + // seek to the current block in the map file + mInputFile.seek(subFileParameter.startAddress + currentBlockPointer); + + // read the current block into the buffer + if (!mReadBuffer.readFromFile(currentBlockSize)) { + // skip the current block + LOG.warning("reading current block has failed: " + currentBlockSize); + return; + } + + // calculate the top-left coordinates of the underlying tile + double tileLatitudeDeg = MercatorProjection.tileYToLatitude(subFileParameter.boundaryTileTop + row, + subFileParameter.baseZoomLevel); + double tileLongitudeDeg = MercatorProjection.tileXToLongitude(subFileParameter.boundaryTileLeft + + column, subFileParameter.baseZoomLevel); + mTileLatitude = (int) (tileLatitudeDeg * 1000000); + mTileLongitude = (int) (tileLongitudeDeg * 1000000); + + try { + processBlock(queryParameters, subFileParameter, mapDatabaseCallback); + } catch (ArrayIndexOutOfBoundsException e) { + LOG.log(Level.SEVERE, null, e); + } + } + } + + // the query is finished, was the water flag set for all blocks? + // if (queryIsWater && queryReadWaterInfo) { + // Tag[] tags = new Tag[1]; + // tags[0] = TAG_NATURAL_WATER; + // + // System.arraycopy(WATER_TILE_COORDINATES, 0, mWayNodes, mWayNodePosition, 8); + // mWayNodePosition += 8; + // mapDatabaseCallback.renderWaterBackground(tags, wayDataContainer); + // } + + } + + /** + * Processes the block signature, if present. + * + * @return true if the block signature could be processed successfully, false otherwise. + */ + private boolean processBlockSignature() { + if (mDebugFile) { + // get and check the block signature + mSignatureBlock = mReadBuffer.readUTF8EncodedString(SIGNATURE_LENGTH_BLOCK); + if (!mSignatureBlock.startsWith("###TileStart")) { + LOG.warning("invalid block signature: " + mSignatureBlock); + return false; + } + } + return true; + } + + /** + * Processes the given number of POIs. + * + * @param mapDatabaseCallback + * the callback which handles the extracted POIs. + * @param numberOfPois + * how many POIs should be processed. + * @return true if the POIs could be processed successfully, false otherwise. + */ + private boolean processPOIs(IMapDatabaseCallback mapDatabaseCallback, int numberOfPois) { + // List tags = new ArrayList(); + Tag[] poiTags = mMapFileHeader.getMapFileInfo().poiTags; + Tag[] tags = null; + + for (int elementCounter = numberOfPois; elementCounter != 0; --elementCounter) { + if (mDebugFile) { + // get and check the POI signature + mSignaturePoi = mReadBuffer.readUTF8EncodedString(SIGNATURE_LENGTH_POI); + if (!mSignaturePoi.startsWith("***POIStart")) { + LOG.warning("invalid POI signature: " + mSignaturePoi); + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + return false; + } + } + + // get the POI latitude offset (VBE-S) + int latitude = mTileLatitude + mReadBuffer.readSignedInt(); + + // get the POI longitude offset (VBE-S) + int longitude = mTileLongitude + mReadBuffer.readSignedInt(); + + // get the special byte which encodes multiple flags + byte specialByte = mReadBuffer.readByte(); + + // bit 1-4 represent the layer + byte layer = (byte) ((specialByte & POI_LAYER_BITMASK) >>> POI_LAYER_SHIFT); + // bit 5-8 represent the number of tag IDs + byte numberOfTags = (byte) (specialByte & POI_NUMBER_OF_TAGS_BITMASK); + + // boolean changed = false; + + if (numberOfTags != 0) { + tags = mReadBuffer.readTags(poiTags, numberOfTags); + // changed = true; + } + if (tags == null) + return false; + + // get the feature bitmask (1 byte) + byte featureByte = mReadBuffer.readByte(); + + // bit 1-3 enable optional features + boolean featureName = (featureByte & POI_FEATURE_NAME) != 0; + boolean featureHouseNumber = (featureByte & POI_FEATURE_HOUSE_NUMBER) != 0; + boolean featureElevation = (featureByte & POI_FEATURE_ELEVATION) != 0; + + // check if the POI has a name + if (featureName) { + mReadBuffer.getPositionAndSkip(); + } + + // check if the POI has a house number + if (featureHouseNumber) { + mReadBuffer.getPositionAndSkip(); + } + + // check if the POI has an elevation + if (featureElevation) { + mReadBuffer.readSignedInt(); + // mReadBuffer.getPositionAndSkip();// tags.add(new Tag(Tag.TAG_KEY_ELE, + // Integer.toString(mReadBuffer.readSignedInt()))); + } + + mapDatabaseCallback.renderPointOfInterest(layer, latitude, longitude, tags); + + } + + return true; + } + + private int[] processWayDataBlock(boolean doubleDeltaEncoding) { + // get and check the number of way coordinate blocks (VBE-U) + int numBlocks = mReadBuffer.readUnsignedInt(); + if (numBlocks < 1 || numBlocks > Short.MAX_VALUE) { + LOG.warning("invalid number of way coordinate blocks: " + numBlocks); + return null; + } + + int[] wayLengths = new int[numBlocks]; + + mWayNodePosition = 0; + + // read the way coordinate blocks + for (int coordinateBlock = 0; coordinateBlock < numBlocks; ++coordinateBlock) { + // get and check the number of way nodes (VBE-U) + int numWayNodes = mReadBuffer.readUnsignedInt(); + + if (numWayNodes < 2 || numWayNodes > MAXIMUM_WAY_NODES_SEQUENCE_LENGTH) { + LOG.warning("invalid number of way nodes: " + numWayNodes); + logDebugSignatures(); + return null; + } + + // each way node consists of latitude and longitude + int len = numWayNodes * 2; + + if (doubleDeltaEncoding) { + len = decodeWayNodesDoubleDelta(len); + } else { + len = decodeWayNodesSingleDelta(len); + } + wayLengths[coordinateBlock] = len; + } + + return wayLengths; + } + + private int decodeWayNodesDoubleDelta(int length) { + int[] buffer = mIntBuffer; + float[] outBuffer = mWayNodes; + + mReadBuffer.readSignedInt(buffer, length); + + int floatPos = mWayNodePosition; + + // get the first way node latitude offset (VBE-S) + int wayNodeLatitude = mTileLatitude + buffer[0]; + + // get the first way node longitude offset (VBE-S) + int wayNodeLongitude = mTileLongitude + buffer[1]; + + // store the first way node + outBuffer[floatPos++] = wayNodeLongitude; + outBuffer[floatPos++] = wayNodeLatitude; + + int singleDeltaLatitude = 0; + int singleDeltaLongitude = 0; + + int cnt = 2, nLon, nLat, dLat, dLon; + + for (int pos = 2; pos < length; pos += 2) { + + singleDeltaLatitude = buffer[pos] + singleDeltaLatitude; + nLat = wayNodeLatitude + singleDeltaLatitude; + dLat = nLat - wayNodeLatitude; + wayNodeLatitude = nLat; + + singleDeltaLongitude = buffer[pos + 1] + singleDeltaLongitude; + nLon = wayNodeLongitude + singleDeltaLongitude; + dLon = nLon - wayNodeLongitude; + wayNodeLongitude = nLon; + + if (dLon > minLon || dLon < -minLon || dLat > minLat || dLat < -minLat || (pos == length - 2)) { + outBuffer[floatPos++] = nLon; + outBuffer[floatPos++] = nLat; + cnt += 2; + } + } + + mWayNodePosition = floatPos; + + return cnt; + } + + private int decodeWayNodesSingleDelta(int length) { + int[] buffer = mIntBuffer; + float[] outBuffer = mWayNodes; + mReadBuffer.readSignedInt(buffer, length); + + int floatPos = mWayNodePosition; + + // get the first way node latitude single-delta offset (VBE-S) + int wayNodeLatitude = mTileLatitude + buffer[0]; + + // get the first way node longitude single-delta offset (VBE-S) + int wayNodeLongitude = mTileLongitude + buffer[1]; + + // store the first way node + outBuffer[floatPos++] = wayNodeLongitude; + outBuffer[floatPos++] = wayNodeLatitude; + + int cnt = 2, nLon, nLat, dLat, dLon; + + for (int pos = 2; pos < length; pos += 2) { + + nLat = wayNodeLatitude + buffer[pos]; + dLat = nLat - wayNodeLatitude; + wayNodeLatitude = nLat; + + nLon = wayNodeLongitude + buffer[pos + 1]; + dLon = nLon - wayNodeLongitude; + wayNodeLongitude = nLon; + + if (dLon > minLon || dLon < -minLon || dLat > minLat || dLat < -minLat || (pos == length - 2)) { + outBuffer[floatPos++] = nLon; + outBuffer[floatPos++] = nLat; + cnt += 2; + } + } + + mWayNodePosition = floatPos; + return cnt; + } + + private int stringOffset = -1; + + /* + * (non-Javadoc) + * @see org.mapsforge.map.reader.IMapDatabase#readString(int) + */ + @Override + public String readString(int position) { + return mReadBuffer.readUTF8EncodedStringAt(stringOffset + position); + } + + /** + * Processes the given number of ways. + * + * @param queryParameters + * the parameters of the current query. + * @param mapDatabaseCallback + * the callback which handles the extracted ways. + * @param numberOfWays + * how many ways should be processed. + * @return true if the ways could be processed successfully, false otherwise. + */ + private boolean processWays(QueryParameters queryParameters, IMapDatabaseCallback mapDatabaseCallback, + int numberOfWays) { + + Tag[] tags = null; + Tag[] wayTags = mMapFileHeader.getMapFileInfo().wayTags; + int[] textPos = new int[3]; + // float[] labelPosition; + boolean skippedWays = false; + int wayDataBlocks; + + // skip string block + int stringsSize = mReadBuffer.readUnsignedInt(); + stringOffset = mReadBuffer.getBufferPosition(); + mReadBuffer.skipBytes(stringsSize); + + for (int elementCounter = numberOfWays; elementCounter != 0; --elementCounter) { + if (mDebugFile) { + // get and check the way signature + mSignatureWay = mReadBuffer.readUTF8EncodedString(SIGNATURE_LENGTH_WAY); + if (!mSignatureWay.startsWith("---WayStart")) { + LOG.warning("invalid way signature: " + mSignatureWay); + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + return false; + } + } + + if (queryParameters.useTileBitmask) { + elementCounter = mReadBuffer.skipWays(queryParameters.queryTileBitmask, elementCounter); + + if (elementCounter == 0) + return true; + + if (elementCounter < 0) + return false; + + if (mReadBuffer.lastTagPosition > 0) { + int pos = mReadBuffer.getBufferPosition(); + mReadBuffer.setBufferPosition(mReadBuffer.lastTagPosition); + + byte numberOfTags = (byte) (mReadBuffer.readByte() & WAY_NUMBER_OF_TAGS_BITMASK); + + tags = mReadBuffer.readTags(wayTags, numberOfTags); + if (tags == null) + return false; + + skippedWays = true; + + mReadBuffer.setBufferPosition(pos); + } + } else { + int wayDataSize = mReadBuffer.readUnsignedInt(); + if (wayDataSize < 0) { + LOG.warning("invalid way data size: " + wayDataSize); + if (mDebugFile) { + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + } + LOG.warning("EEEEEK way... 2"); + return false; + } + + // ignore the way tile bitmask (2 bytes) + mReadBuffer.skipBytes(2); + } + + // get the special byte which encodes multiple flags + byte specialByte = mReadBuffer.readByte(); + + // bit 1-4 represent the layer + byte layer = (byte) ((specialByte & WAY_LAYER_BITMASK) >>> WAY_LAYER_SHIFT); + // bit 5-8 represent the number of tag IDs + byte numberOfTags = (byte) (specialByte & WAY_NUMBER_OF_TAGS_BITMASK); + + boolean changed = skippedWays; + skippedWays = false; + + if (numberOfTags != 0) { + tags = mReadBuffer.readTags(wayTags, numberOfTags); + changed = true; + } + if (tags == null) + return false; + + // get the feature bitmask (1 byte) + byte featureByte = mReadBuffer.readByte(); + + // bit 1-6 enable optional features + boolean featureWayDoubleDeltaEncoding = (featureByte & WAY_FEATURE_DOUBLE_DELTA_ENCODING) != 0; + + // check if the way has a name + if ((featureByte & WAY_FEATURE_NAME) != 0) + textPos[0] = mReadBuffer.readUnsignedInt(); + else + textPos[0] = -1; + + // check if the way has a house number + if ((featureByte & WAY_FEATURE_HOUSE_NUMBER) != 0) + textPos[1] = mReadBuffer.readUnsignedInt(); + else + textPos[1] = -1; + + // check if the way has a reference + if ((featureByte & WAY_FEATURE_REF) != 0) + textPos[2] = mReadBuffer.readUnsignedInt(); + else + textPos[2] = -1; + + if ((featureByte & WAY_FEATURE_LABEL_POSITION) != 0) + // labelPosition = + readOptionalLabelPosition(); + // else + // labelPosition = null; + + if ((featureByte & WAY_FEATURE_DATA_BLOCKS_BYTE) != 0) { + wayDataBlocks = mReadBuffer.readUnsignedInt(); + + if (wayDataBlocks < 1) { + LOG.warning("invalid number of way data blocks: " + wayDataBlocks); + logDebugSignatures(); + return false; + } + } else { + wayDataBlocks = 1; + } + + for (int wayDataBlock = 0; wayDataBlock < wayDataBlocks; ++wayDataBlock) { + int[] wayLengths = processWayDataBlock(featureWayDoubleDeltaEncoding); + if (wayLengths == null) + return false; + + // wayDataContainer.textPos = textPos; + mapDatabaseCallback.renderWay(layer, tags, mWayNodes, wayLengths, changed); + } + } + + return true; + } + + private float[] readOptionalLabelPosition() { + float[] labelPosition = new float[2]; + + // get the label position latitude offset (VBE-S) + labelPosition[1] = mTileLatitude + mReadBuffer.readSignedInt(); + + // get the label position longitude offset (VBE-S) + labelPosition[0] = mTileLongitude + mReadBuffer.readSignedInt(); + + return labelPosition; + } + + // private int readOptionalWayDataBlocksByte(boolean featureWayDataBlocksByte) { + // if (featureWayDataBlocksByte) { + // // get and check the number of way data blocks (VBE-U) + // return mReadBuffer.readUnsignedInt(); + // } + // // only one way data block exists + // return 1; + // } + + private int[][] readZoomTable(SubFileParameter subFileParameter) { + int rows = subFileParameter.zoomLevelMax - subFileParameter.zoomLevelMin + 1; + int[][] zoomTable = new int[rows][2]; + + int cumulatedNumberOfPois = 0; + int cumulatedNumberOfWays = 0; + + for (int row = 0; row < rows; ++row) { + cumulatedNumberOfPois += mReadBuffer.readUnsignedInt(); + cumulatedNumberOfWays += mReadBuffer.readUnsignedInt(); + + if (cumulatedNumberOfPois < 0 || cumulatedNumberOfPois > MAXIMUM_ZOOM_TABLE_OBJECTS) { + LOG.warning("invalid cumulated number of POIs in row " + row + ' ' + cumulatedNumberOfPois); + if (mDebugFile) { + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + } + return null; + } else if (cumulatedNumberOfWays < 0 || cumulatedNumberOfWays > MAXIMUM_ZOOM_TABLE_OBJECTS) { + LOG.warning("invalid cumulated number of ways in row " + row + ' ' + cumulatedNumberOfWays); + if (mMapFileHeader.getMapFileInfo().debugFile) { + LOG.warning(DEBUG_SIGNATURE_BLOCK + mSignatureBlock); + } + return null; + } + + zoomTable[row][0] = cumulatedNumberOfPois; + zoomTable[row][1] = cumulatedNumberOfWays; + } + + return zoomTable; + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/QueryCalculations.java b/src/org/mapsforge/mapdatabase/mapfile/QueryCalculations.java new file mode 100644 index 00000000..afcfc529 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/QueryCalculations.java @@ -0,0 +1,167 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile; + +import org.mapsforge.core.Tile; +import org.mapsforge.mapdatabase.mapfile.header.SubFileParameter; + +final class QueryCalculations { + private static int getFirstLevelTileBitmask(Tile tile) { + if (tile.tileX % 2 == 0 && tile.tileY % 2 == 0) { + // upper left quadrant + return 0xcc00; + } else if (tile.tileX % 2 == 1 && tile.tileY % 2 == 0) { + // upper right quadrant + return 0x3300; + } else if (tile.tileX % 2 == 0 && tile.tileY % 2 == 1) { + // lower left quadrant + return 0xcc; + } else { + // lower right quadrant + return 0x33; + } + } + + private static int getSecondLevelTileBitmaskLowerLeft(long subtileX, long subtileY) { + if (subtileX % 2 == 0 && subtileY % 2 == 0) { + // upper left sub-tile + return 0x80; + } else if (subtileX % 2 == 1 && subtileY % 2 == 0) { + // upper right sub-tile + return 0x40; + } else if (subtileX % 2 == 0 && subtileY % 2 == 1) { + // lower left sub-tile + return 0x8; + } else { + // lower right sub-tile + return 0x4; + } + } + + private static int getSecondLevelTileBitmaskLowerRight(long subtileX, long subtileY) { + if (subtileX % 2 == 0 && subtileY % 2 == 0) { + // upper left sub-tile + return 0x20; + } else if (subtileX % 2 == 1 && subtileY % 2 == 0) { + // upper right sub-tile + return 0x10; + } else if (subtileX % 2 == 0 && subtileY % 2 == 1) { + // lower left sub-tile + return 0x2; + } else { + // lower right sub-tile + return 0x1; + } + } + + private static int getSecondLevelTileBitmaskUpperLeft(long subtileX, long subtileY) { + if (subtileX % 2 == 0 && subtileY % 2 == 0) { + // upper left sub-tile + return 0x8000; + } else if (subtileX % 2 == 1 && subtileY % 2 == 0) { + // upper right sub-tile + return 0x4000; + } else if (subtileX % 2 == 0 && subtileY % 2 == 1) { + // lower left sub-tile + return 0x800; + } else { + // lower right sub-tile + return 0x400; + } + } + + private static int getSecondLevelTileBitmaskUpperRight(long subtileX, long subtileY) { + if (subtileX % 2 == 0 && subtileY % 2 == 0) { + // upper left sub-tile + return 0x2000; + } else if (subtileX % 2 == 1 && subtileY % 2 == 0) { + // upper right sub-tile + return 0x1000; + } else if (subtileX % 2 == 0 && subtileY % 2 == 1) { + // lower left sub-tile + return 0x200; + } else { + // lower right sub-tile + return 0x100; + } + } + + static void calculateBaseTiles(QueryParameters queryParameters, Tile tile, SubFileParameter subFileParameter) { + if (tile.zoomLevel < subFileParameter.baseZoomLevel) { + // calculate the XY numbers of the upper left and lower right sub-tiles + int zoomLevelDifference = subFileParameter.baseZoomLevel - tile.zoomLevel; + queryParameters.fromBaseTileX = tile.tileX << zoomLevelDifference; + queryParameters.fromBaseTileY = tile.tileY << zoomLevelDifference; + queryParameters.toBaseTileX = queryParameters.fromBaseTileX + (1 << zoomLevelDifference) - 1; + queryParameters.toBaseTileY = queryParameters.fromBaseTileY + (1 << zoomLevelDifference) - 1; + queryParameters.useTileBitmask = false; + } else if (tile.zoomLevel > subFileParameter.baseZoomLevel) { + // calculate the XY numbers of the parent base tile + int zoomLevelDifference = tile.zoomLevel - subFileParameter.baseZoomLevel; + queryParameters.fromBaseTileX = tile.tileX >>> zoomLevelDifference; + queryParameters.fromBaseTileY = tile.tileY >>> zoomLevelDifference; + queryParameters.toBaseTileX = queryParameters.fromBaseTileX; + queryParameters.toBaseTileY = queryParameters.fromBaseTileY; + queryParameters.useTileBitmask = true; + queryParameters.queryTileBitmask = calculateTileBitmask(tile, zoomLevelDifference); + } else { + // use the tile XY numbers of the requested tile + queryParameters.fromBaseTileX = tile.tileX; + queryParameters.fromBaseTileY = tile.tileY; + queryParameters.toBaseTileX = queryParameters.fromBaseTileX; + queryParameters.toBaseTileY = queryParameters.fromBaseTileY; + queryParameters.useTileBitmask = false; + } + } + + static void calculateBlocks(QueryParameters queryParameters, SubFileParameter subFileParameter) { + // calculate the blocks in the file which need to be read + queryParameters.fromBlockX = Math.max(queryParameters.fromBaseTileX - subFileParameter.boundaryTileLeft, 0); + queryParameters.fromBlockY = Math.max(queryParameters.fromBaseTileY - subFileParameter.boundaryTileTop, 0); + queryParameters.toBlockX = Math.min(queryParameters.toBaseTileX - subFileParameter.boundaryTileLeft, + subFileParameter.blocksWidth - 1); + queryParameters.toBlockY = Math.min(queryParameters.toBaseTileY - subFileParameter.boundaryTileTop, + subFileParameter.blocksHeight - 1); + } + + static int calculateTileBitmask(Tile tile, int zoomLevelDifference) { + if (zoomLevelDifference == 1) { + return getFirstLevelTileBitmask(tile); + } + + // calculate the XY numbers of the second level sub-tile + long subtileX = tile.tileX >>> (zoomLevelDifference - 2); + long subtileY = tile.tileY >>> (zoomLevelDifference - 2); + + // calculate the XY numbers of the parent tile + long parentTileX = subtileX >>> 1; + long parentTileY = subtileY >>> 1; + + // determine the correct bitmask for all 16 sub-tiles + if (parentTileX % 2 == 0 && parentTileY % 2 == 0) { + return getSecondLevelTileBitmaskUpperLeft(subtileX, subtileY); + } else if (parentTileX % 2 == 1 && parentTileY % 2 == 0) { + return getSecondLevelTileBitmaskUpperRight(subtileX, subtileY); + } else if (parentTileX % 2 == 0 && parentTileY % 2 == 1) { + return getSecondLevelTileBitmaskLowerLeft(subtileX, subtileY); + } else { + return getSecondLevelTileBitmaskLowerRight(subtileX, subtileY); + } + } + + private QueryCalculations() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/QueryParameters.java b/src/org/mapsforge/mapdatabase/mapfile/QueryParameters.java new file mode 100644 index 00000000..60c1e195 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/QueryParameters.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile; + +class QueryParameters { + long fromBaseTileX; + long fromBaseTileY; + long fromBlockX; + long fromBlockY; + int queryTileBitmask; + int queryZoomLevel; + long toBaseTileX; + long toBaseTileY; + long toBlockX; + long toBlockY; + boolean useTileBitmask; + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("QueryParameters [fromBaseTileX="); + stringBuilder.append(this.fromBaseTileX); + stringBuilder.append(", fromBaseTileY="); + stringBuilder.append(this.fromBaseTileY); + stringBuilder.append(", fromBlockX="); + stringBuilder.append(this.fromBlockX); + stringBuilder.append(", fromBlockY="); + stringBuilder.append(this.fromBlockY); + stringBuilder.append(", queryTileBitmask="); + stringBuilder.append(this.queryTileBitmask); + stringBuilder.append(", queryZoomLevel="); + stringBuilder.append(this.queryZoomLevel); + stringBuilder.append(", toBaseTileX="); + stringBuilder.append(this.toBaseTileX); + stringBuilder.append(", toBaseTileY="); + stringBuilder.append(this.toBaseTileY); + stringBuilder.append(", toBlockX="); + stringBuilder.append(this.toBlockX); + stringBuilder.append(", toBlockY="); + stringBuilder.append(this.toBlockY); + stringBuilder.append(", useTileBitmask="); + stringBuilder.append(this.useTileBitmask); + stringBuilder.append("]"); + return stringBuilder.toString(); + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/ReadBuffer.java b/src/org/mapsforge/mapdatabase/mapfile/ReadBuffer.java new file mode 100644 index 00000000..8e1cb7c9 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/ReadBuffer.java @@ -0,0 +1,535 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.util.logging.Logger; + +import org.mapsforge.core.Tag; + +/** + * Reads from a {@link RandomAccessFile} into a buffer and decodes the data. + */ +public class ReadBuffer { + private static final String CHARSET_UTF8 = "UTF-8"; + private static final Logger LOG = Logger.getLogger(ReadBuffer.class.getName()); + + /** + * Maximum buffer size which is supported by this implementation. + */ + static final int MAXIMUM_BUFFER_SIZE = 8000000; + + private byte[] mBufferData; + private int mBufferPosition; + private final RandomAccessFile mInputFile; + + ReadBuffer(RandomAccessFile inputFile) { + mInputFile = inputFile; + } + + /** + * Returns one signed byte from the read buffer. + * + * @return the byte value. + */ + public byte readByte() { + return mBufferData[mBufferPosition++]; + } + + /** + * Reads the given amount of bytes from the file into the read buffer and resets the internal buffer position. If + * the capacity of the read buffer is too small, a larger one is created automatically. + * + * @param length + * the amount of bytes to read from the file. + * @return true if the whole data was read successfully, false otherwise. + * @throws IOException + * if an error occurs while reading the file. + */ + public boolean readFromFile(int length) throws IOException { + // ensure that the read buffer is large enough + if (mBufferData == null || mBufferData.length < length) { + // ensure that the read buffer is not too large + if (length > MAXIMUM_BUFFER_SIZE) { + LOG.warning("invalid read length: " + length); + return false; + } + mBufferData = new byte[length]; + } + + mBufferPosition = 0; + + // reset the buffer position and read the data into the buffer + // bufferPosition = 0; + return mInputFile.read(mBufferData, 0, length) == length; + } + + /** + * Converts four bytes from the read buffer to a signed int. + *

+ * The byte order is big-endian. + * + * @return the int value. + */ + public int readInt() { + int pos = mBufferPosition; + byte[] data = mBufferData; + mBufferPosition += 4; + + return data[pos] << 24 + | (data[pos + 1] & 0xff) << 16 + | (data[pos + 2] & 0xff) << 8 + | (data[pos + 3] & 0xff); + } + + /** + * Converts eight bytes from the read buffer to a signed long. + *

+ * The byte order is big-endian. + * + * @return the long value. + */ + public long readLong() { + int pos = mBufferPosition; + byte[] data = mBufferData; + mBufferPosition += 8; + + return (data[pos] & 0xffL) << 56 + | (data[pos + 1] & 0xffL) << 48 + | (data[pos + 2] & 0xffL) << 40 + | (data[pos + 3] & 0xffL) << 32 + | (data[pos + 4] & 0xffL) << 24 + | (data[pos + 5] & 0xffL) << 16 + | (data[pos + 6] & 0xffL) << 8 + | (data[pos + 7] & 0xffL); + + } + + /** + * Converts two bytes from the read buffer to a signed int. + *

+ * The byte order is big-endian. + * + * @return the int value. + */ + public int readShort() { + mBufferPosition += 2; + return mBufferData[mBufferPosition - 2] << 8 | (mBufferData[mBufferPosition - 1] & 0xff); + } + + /** + * Converts a variable amount of bytes from the read buffer to a signed int. + *

+ * The first bit is for continuation info, the other six (last byte) or seven (all other bytes) bits are for data. + * The second bit in the last byte indicates the sign of the number. + * + * @return the value. + */ + public int readSignedInt() { + int pos = mBufferPosition; + byte[] data = mBufferData; + int flag; + + if ((data[pos] & 0x80) == 0) { + mBufferPosition += 1; + flag = ((data[pos] & 0x40) >> 6); + + return ((data[pos] & 0x3f) ^ -flag) + flag; + } + + if ((data[pos + 1] & 0x80) == 0) { + mBufferPosition += 2; + flag = ((data[pos + 1] & 0x40) >> 6); + + return (((data[pos] & 0x7f) + | (data[pos + 1] & 0x3f) << 7) ^ -flag) + flag; + + } + + if ((data[pos + 2] & 0x80) == 0) { + mBufferPosition += 3; + flag = ((data[pos + 2] & 0x40) >> 6); + + return (((data[pos] & 0x7f) + | (data[pos + 1] & 0x7f) << 7 + | (data[pos + 2] & 0x3f) << 14) ^ -flag) + flag; + + } + + if ((data[pos + 3] & 0x80) == 0) { + mBufferPosition += 4; + flag = ((data[pos + 3] & 0x40) >> 6); + + return (((data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14) + | ((data[pos + 3] & 0x3f) << 21)) ^ -flag) + flag; + } + + mBufferPosition += 5; + flag = ((data[pos + 4] & 0x40) >> 6); + + return ((((data[pos] & 0x7f) + | (data[pos + 1] & 0x7f) << 7 + | (data[pos + 2] & 0x7f) << 14 + | (data[pos + 3] & 0x7f) << 21 + | (data[pos + 4] & 0x3f) << 28)) ^ -flag) + flag; + + } + + /** + * Converts a variable amount of bytes from the read buffer to a signed int array. + *

+ * The first bit is for continuation info, the other six (last byte) or seven (all other bytes) bits are for data. + * The second bit in the last byte indicates the sign of the number. + * + * @param values + * result values + * @param length + * number of values to read + */ + public void readSignedInt(int[] values, int length) { + int pos = mBufferPosition; + byte[] data = mBufferData; + int flag; + + for (int i = 0; i < length; i++) { + + if ((data[pos] & 0x80) == 0) { + + flag = ((data[pos] & 0x40) >> 6); + + values[i] = ((data[pos] & 0x3f) ^ -flag) + flag; + pos += 1; + + } else if ((data[pos + 1] & 0x80) == 0) { + + flag = ((data[pos + 1] & 0x40) >> 6); + + values[i] = (((data[pos] & 0x7f) + | ((data[pos + 1] & 0x3f) << 7)) ^ -flag) + flag; + pos += 2; + + } else if ((data[pos + 2] & 0x80) == 0) { + + flag = ((data[pos + 2] & 0x40) >> 6); + + values[i] = (((data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x3f) << 14)) ^ -flag) + flag; + pos += 3; + + } else if ((data[pos + 3] & 0x80) == 0) { + + flag = ((data[pos + 3] & 0x40) >> 6); + + values[i] = (((data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14) + | ((data[pos + 3] & 0x3f) << 21)) ^ -flag) + flag; + + pos += 4; + } else { + flag = ((data[pos + 4] & 0x40) >> 6); + + values[i] = ((((data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14) + | ((data[pos + 3] & 0x7f) << 21) + | ((data[pos + 4] & 0x3f) << 28))) ^ -flag) + flag; + + pos += 5; + } + } + + mBufferPosition = pos; + } + + // public void readSignedInt(int[] values, int length) { + // int pos = mBufferPosition; + // byte[] data = mBufferData; + // + // for (int i = 0; i < length; i++) { + // + // if ((data[pos] & 0x80) == 0) { + // if ((data[pos] & 0x40) != 0) + // values[i] = -(data[pos] & 0x3f); + // else + // values[i] = (data[pos] & 0x3f); + // pos += 1; + // } else if ((data[pos + 1] & 0x80) == 0) { + // if ((data[pos + 1] & 0x40) != 0) + // values[i] = -((data[pos] & 0x7f) + // | ((data[pos + 1] & 0x3f) << 7)); + // else + // values[i] = (data[pos] & 0x7f) + // | ((data[pos + 1] & 0x3f) << 7); + // pos += 2; + // } else if ((data[pos + 2] & 0x80) == 0) { + // if ((data[pos + 2] & 0x40) != 0) + // values[i] = -((data[pos] & 0x7f) + // | ((data[pos + 1] & 0x7f) << 7) + // | ((data[pos + 2] & 0x3f) << 14)); + // else + // values[i] = (data[pos] & 0x7f) + // | ((data[pos + 1] & 0x7f) << 7) + // | ((data[pos + 2] & 0x3f) << 14); + // pos += 3; + // } else if ((data[pos + 3] & 0x80) == 0) { + // if ((data[pos + 3] & 0x40) != 0) + // values[i] = -((data[pos] & 0x7f) + // | ((data[pos + 1] & 0x7f) << 7) + // | ((data[pos + 2] & 0x7f) << 14) + // | ((data[pos + 3] & 0x3f) << 21)); + // else + // values[i] = (data[pos] & 0x7f) + // | ((data[pos + 1] & 0x7f) << 7) + // | ((data[pos + 2] & 0x7f) << 14) + // | ((data[pos + 3] & 0x3f) << 21); + // pos += 4; + // } else { + // if ((data[pos + 4] & 0x40) != 0) + // values[i] = -((data[pos] & 0x7f) + // | ((data[pos + 1] & 0x7f) << 7) + // | ((data[pos + 2] & 0x7f) << 14) + // | ((data[pos + 3] & 0x7f) << 21) + // | ((data[pos + 4] & 0x3f) << 28)); + // else + // values[i] = ((data[pos] & 0x7f) + // | ((data[pos + 1] & 0x7f) << 7) + // | ((data[pos + 2] & 0x7f) << 14) + // | ((data[pos + 3] & 0x7f) << 21) + // | ((data[pos + 4] & 0x3f) << 28)); + // pos += 5; + // } + // } + // + // mBufferPosition = pos; + // } + + /** + * Converts a variable amount of bytes from the read buffer to an unsigned int. + *

+ * The first bit is for continuation info, the other seven bits are for data. + * + * @return the int value. + */ + public int readUnsignedInt() { + int pos = mBufferPosition; + byte[] data = mBufferData; + + if ((data[pos] & 0x80) == 0) { + mBufferPosition += 1; + return (data[pos] & 0x7f); + } + + if ((data[pos + 1] & 0x80) == 0) { + mBufferPosition += 2; + return (data[pos] & 0x7f) + | (data[pos + 1] & 0x7f) << 7; + } + + if ((data[pos + 2] & 0x80) == 0) { + mBufferPosition += 3; + return (data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14); + } + + if ((data[pos + 3] & 0x80) == 0) { + mBufferPosition += 4; + return (data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14) + | ((data[pos + 3] & 0x7f) << 21); + } + + mBufferPosition += 5; + return (data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14) + | ((data[pos + 3] & 0x7f) << 21) + | ((data[pos + 4] & 0x7f) << 28); + } + + /** + * Decodes a variable amount of bytes from the read buffer to a string. + * + * @return the UTF-8 decoded string (may be null). + */ + public String readUTF8EncodedString() { + return readUTF8EncodedString(readUnsignedInt()); + } + + /** + * @return ... + */ + public int getPositionAndSkip() { + int pos = mBufferPosition; + int length = readUnsignedInt(); + skipBytes(length); + return pos; + } + + /** + * Decodes the given amount of bytes from the read buffer to a string. + * + * @param stringLength + * the length of the string in bytes. + * @return the UTF-8 decoded string (may be null). + */ + public String readUTF8EncodedString(int stringLength) { + if (stringLength > 0 && mBufferPosition + stringLength <= mBufferData.length) { + mBufferPosition += stringLength; + try { + return new String(mBufferData, mBufferPosition - stringLength, stringLength, CHARSET_UTF8); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + LOG.warning("invalid string length: " + stringLength); + return null; + } + + /** + * Decodes a variable amount of bytes from the read buffer to a string. + * + * @param position + * buffer offset position of string + * @return the UTF-8 decoded string (may be null). + */ + public String readUTF8EncodedStringAt(int position) { + int curPosition = mBufferPosition; + mBufferPosition = position; + String result = readUTF8EncodedString(readUnsignedInt()); + mBufferPosition = curPosition; + return result; + } + + /** + * @return the current buffer position. + */ + int getBufferPosition() { + return mBufferPosition; + } + + /** + * @return the current size of the read buffer. + */ + int getBufferSize() { + return mBufferData.length; + } + + /** + * Sets the buffer position to the given offset. + * + * @param bufferPosition + * the buffer position. + */ + void setBufferPosition(int bufferPosition) { + mBufferPosition = bufferPosition; + } + + /** + * Skips the given number of bytes in the read buffer. + * + * @param bytes + * the number of bytes to skip. + */ + void skipBytes(int bytes) { + mBufferPosition += bytes; + } + + Tag[] readTags(Tag[] wayTags, byte numberOfTags) { + Tag[] tags = new Tag[numberOfTags]; + + int maxTag = wayTags.length; + + for (byte i = 0; i < numberOfTags; i++) { + int tagId = readUnsignedInt(); + if (tagId < 0 || tagId >= maxTag) { + LOG.warning("invalid tag ID: " + tagId); + return null; + } + tags[i] = wayTags[tagId]; + } + return tags; + } + + private static final int WAY_NUMBER_OF_TAGS_BITMASK = 0x0f; + int lastTagPosition; + + int skipWays(int queryTileBitmask, int elements) { + int pos = mBufferPosition; + byte[] data = mBufferData; + int cnt = elements; + int skip; + + lastTagPosition = -1; + + while (cnt > 0) { + // read way size (unsigned int) + if ((data[pos] & 0x80) == 0) { + skip = (data[pos] & 0x7f); + pos += 1; + } else if ((data[pos + 1] & 0x80) == 0) { + skip = (data[pos] & 0x7f) + | (data[pos + 1] & 0x7f) << 7; + pos += 2; + } else if ((data[pos + 2] & 0x80) == 0) { + skip = (data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14); + pos += 3; + } else if ((data[pos + 3] & 0x80) == 0) { + skip = (data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14) + | ((data[pos + 3] & 0x7f) << 21); + pos += 4; + } else { + skip = (data[pos] & 0x7f) + | ((data[pos + 1] & 0x7f) << 7) + | ((data[pos + 2] & 0x7f) << 14) + | ((data[pos + 3] & 0x7f) << 21) + | ((data[pos + 4] & 0x7f) << 28); + pos += 5; + } + // invalid way size + if (skip < 0) { + mBufferPosition = pos; + return -1; + } + + // check if way matches queryTileBitmask + if ((((data[pos] << 8) | (data[pos + 1] & 0xff)) & queryTileBitmask) == 0) { + + // remember last tags position + if ((data[pos + 2] & WAY_NUMBER_OF_TAGS_BITMASK) != 0) + lastTagPosition = pos + 2; + + pos += skip; + cnt--; + } else { + pos += 2; + break; + } + } + mBufferPosition = pos; + return cnt; + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/header/MapFileHeader.java b/src/org/mapsforge/mapdatabase/mapfile/header/MapFileHeader.java new file mode 100644 index 00000000..ff04cc54 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/header/MapFileHeader.java @@ -0,0 +1,251 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile.header; + +import java.io.IOException; + +import org.mapsforge.mapdatabase.FileOpenResult; +import org.mapsforge.mapdatabase.mapfile.ReadBuffer; + +/** + * Reads and validates the header data from a binary map file. + */ +public class MapFileHeader { + /** + * Maximum valid base zoom level of a sub-file. + */ + private static final int BASE_ZOOM_LEVEL_MAX = 20; + + /** + * Minimum size of the file header in bytes. + */ + private static final int HEADER_SIZE_MIN = 70; + + /** + * Length of the debug signature at the beginning of the index. + */ + private static final byte SIGNATURE_LENGTH_INDEX = 16; + + /** + * A single whitespace character. + */ + private static final char SPACE = ' '; + + private MapFileInfo mapFileInfo; + private SubFileParameter[] subFileParameters; + private byte zoomLevelMaximum; + private byte zoomLevelMinimum; + + /** + * @return a MapFileInfo containing the header data. + */ + public MapFileInfo getMapFileInfo() { + return this.mapFileInfo; + } + + /** + * @param zoomLevel + * the originally requested zoom level. + * @return the closest possible zoom level which is covered by a sub-file. + */ + public byte getQueryZoomLevel(byte zoomLevel) { + if (zoomLevel > this.zoomLevelMaximum) { + return this.zoomLevelMaximum; + } else if (zoomLevel < this.zoomLevelMinimum) { + return this.zoomLevelMinimum; + } + return zoomLevel; + } + + /** + * @param queryZoomLevel + * the zoom level for which the sub-file parameters are needed. + * @return the sub-file parameters for the given zoom level. + */ + public SubFileParameter getSubFileParameter(int queryZoomLevel) { + return this.subFileParameters[queryZoomLevel]; + } + + /** + * Reads and validates the header block from the map file. + * + * @param readBuffer + * the ReadBuffer for the file data. + * @param fileSize + * the size of the map file in bytes. + * @return a FileOpenResult containing an error message in case of a failure. + * @throws IOException + * if an error occurs while reading the file. + */ + public FileOpenResult readHeader(ReadBuffer readBuffer, long fileSize) throws IOException { + FileOpenResult fileOpenResult = RequiredFields.readMagicByte(readBuffer); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = RequiredFields.readRemainingHeader(readBuffer); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + MapFileInfoBuilder mapFileInfoBuilder = new MapFileInfoBuilder(); + + fileOpenResult = RequiredFields.readFileVersion(readBuffer, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = RequiredFields.readFileSize(readBuffer, fileSize, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = RequiredFields.readMapDate(readBuffer, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = RequiredFields.readBoundingBox(readBuffer, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = RequiredFields.readTilePixelSize(readBuffer, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = RequiredFields.readProjectionName(readBuffer, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = OptionalFields.readOptionalFields(readBuffer, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = RequiredFields.readPoiTags(readBuffer, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = RequiredFields.readWayTags(readBuffer, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = readSubFileParameters(readBuffer, fileSize, mapFileInfoBuilder); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + this.mapFileInfo = mapFileInfoBuilder.build(); + return FileOpenResult.SUCCESS; + } + + private FileOpenResult readSubFileParameters(ReadBuffer readBuffer, long fileSize, + MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the number of sub-files (1 byte) + byte numberOfSubFiles = readBuffer.readByte(); + if (numberOfSubFiles < 1) { + return new FileOpenResult("invalid number of sub-files: " + numberOfSubFiles); + } + mapFileInfoBuilder.numberOfSubFiles = numberOfSubFiles; + + SubFileParameter[] tempSubFileParameters = new SubFileParameter[numberOfSubFiles]; + this.zoomLevelMinimum = Byte.MAX_VALUE; + this.zoomLevelMaximum = Byte.MIN_VALUE; + + // get and check the information for each sub-file + for (byte currentSubFile = 0; currentSubFile < numberOfSubFiles; ++currentSubFile) { + SubFileParameterBuilder subFileParameterBuilder = new SubFileParameterBuilder(); + + // get and check the base zoom level (1 byte) + byte baseZoomLevel = readBuffer.readByte(); + if (baseZoomLevel < 0 || baseZoomLevel > BASE_ZOOM_LEVEL_MAX) { + return new FileOpenResult("invalid base zooom level: " + baseZoomLevel); + } + subFileParameterBuilder.baseZoomLevel = baseZoomLevel; + + // get and check the minimum zoom level (1 byte) + byte zoomLevelMin = readBuffer.readByte(); + if (zoomLevelMin < 0 || zoomLevelMin > 22) { + return new FileOpenResult("invalid minimum zoom level: " + zoomLevelMin); + } + subFileParameterBuilder.zoomLevelMin = zoomLevelMin; + + // get and check the maximum zoom level (1 byte) + byte zoomLevelMax = readBuffer.readByte(); + if (zoomLevelMax < 0 || zoomLevelMax > 22) { + return new FileOpenResult("invalid maximum zoom level: " + zoomLevelMax); + } + subFileParameterBuilder.zoomLevelMax = zoomLevelMax; + + // check for valid zoom level range + if (zoomLevelMin > zoomLevelMax) { + return new FileOpenResult("invalid zoom level range: " + zoomLevelMin + SPACE + zoomLevelMax); + } + + // get and check the start address of the sub-file (8 bytes) + long startAddress = readBuffer.readLong(); + if (startAddress < HEADER_SIZE_MIN || startAddress >= fileSize) { + return new FileOpenResult("invalid start address: " + startAddress); + } + subFileParameterBuilder.startAddress = startAddress; + + long indexStartAddress = startAddress; + if (mapFileInfoBuilder.optionalFields.isDebugFile) { + // the sub-file has an index signature before the index + indexStartAddress += SIGNATURE_LENGTH_INDEX; + } + subFileParameterBuilder.indexStartAddress = indexStartAddress; + + // get and check the size of the sub-file (8 bytes) + long subFileSize = readBuffer.readLong(); + if (subFileSize < 1) { + return new FileOpenResult("invalid sub-file size: " + subFileSize); + } + subFileParameterBuilder.subFileSize = subFileSize; + + subFileParameterBuilder.boundingBox = mapFileInfoBuilder.boundingBox; + + // add the current sub-file to the list of sub-files + tempSubFileParameters[currentSubFile] = subFileParameterBuilder.build(); + + updateZoomLevelInformation(tempSubFileParameters[currentSubFile]); + } + + // create and fill the lookup table for the sub-files + this.subFileParameters = new SubFileParameter[this.zoomLevelMaximum + 1]; + for (int currentMapFile = 0; currentMapFile < numberOfSubFiles; ++currentMapFile) { + SubFileParameter subFileParameter = tempSubFileParameters[currentMapFile]; + for (byte zoomLevel = subFileParameter.zoomLevelMin; zoomLevel <= subFileParameter.zoomLevelMax; ++zoomLevel) { + this.subFileParameters[zoomLevel] = subFileParameter; + } + } + return FileOpenResult.SUCCESS; + } + + private void updateZoomLevelInformation(SubFileParameter subFileParameter) { + // update the global minimum and maximum zoom level information + if (this.zoomLevelMinimum > subFileParameter.zoomLevelMin) { + this.zoomLevelMinimum = subFileParameter.zoomLevelMin; + } + if (this.zoomLevelMaximum < subFileParameter.zoomLevelMax) { + this.zoomLevelMaximum = subFileParameter.zoomLevelMax; + } + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/header/MapFileInfo.java b/src/org/mapsforge/mapdatabase/mapfile/header/MapFileInfo.java new file mode 100644 index 00000000..db3d8935 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/header/MapFileInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile.header; + +import org.mapsforge.core.Tag; +import org.mapsforge.mapdatabase.mapfile.MapDatabase; + +/** + * Contains the immutable metadata of a map file. + * + * @see MapDatabase#getMapFileInfo() + */ +public class MapFileInfo extends org.mapsforge.mapdatabase.MapFileInfo { + + /** + * True if the map file includes debug information, false otherwise. + */ + public final boolean debugFile; + + /** + * The number of sub-files in the map file. + */ + public final byte numberOfSubFiles; + + /** + * The POI tags. + */ + public final Tag[] poiTags; + + /** + * The way tags. + */ + public final Tag[] wayTags; + + /** + * The size of the tiles in pixels. + */ + public final int tilePixelSize; + + MapFileInfo(MapFileInfoBuilder mapFileInfoBuilder) { + super(mapFileInfoBuilder.boundingBox, + mapFileInfoBuilder.optionalFields.startZoomLevel, + mapFileInfoBuilder.optionalFields.startPosition, + mapFileInfoBuilder.projectionName, + mapFileInfoBuilder.mapDate, + mapFileInfoBuilder.fileSize, + mapFileInfoBuilder.fileVersion, + mapFileInfoBuilder.optionalFields.languagePreference, + mapFileInfoBuilder.optionalFields.comment, + mapFileInfoBuilder.optionalFields.createdBy); + + debugFile = mapFileInfoBuilder.optionalFields.isDebugFile; + + numberOfSubFiles = mapFileInfoBuilder.numberOfSubFiles; + poiTags = mapFileInfoBuilder.poiTags; + + tilePixelSize = mapFileInfoBuilder.tilePixelSize; + wayTags = mapFileInfoBuilder.wayTags; + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/header/MapFileInfoBuilder.java b/src/org/mapsforge/mapdatabase/mapfile/header/MapFileInfoBuilder.java new file mode 100644 index 00000000..99fcc0c9 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/header/MapFileInfoBuilder.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile.header; + +import org.mapsforge.core.BoundingBox; +import org.mapsforge.core.Tag; + +class MapFileInfoBuilder { + BoundingBox boundingBox; + long fileSize; + int fileVersion; + long mapDate; + byte numberOfSubFiles; + OptionalFields optionalFields; + Tag[] poiTags; + String projectionName; + int tilePixelSize; + Tag[] wayTags; + + MapFileInfo build() { + return new MapFileInfo(this); + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/header/OptionalFields.java b/src/org/mapsforge/mapdatabase/mapfile/header/OptionalFields.java new file mode 100644 index 00000000..2efbb7a2 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/header/OptionalFields.java @@ -0,0 +1,163 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile.header; + +import org.mapsforge.core.GeoPoint; +import org.mapsforge.mapdatabase.FileOpenResult; +import org.mapsforge.mapdatabase.mapfile.ReadBuffer; + +final class OptionalFields { + /** + * Bitmask for the comment field in the file header. + */ + private static final int HEADER_BITMASK_COMMENT = 0x08; + + /** + * Bitmask for the created by field in the file header. + */ + private static final int HEADER_BITMASK_CREATED_BY = 0x04; + + /** + * Bitmask for the debug flag in the file header. + */ + private static final int HEADER_BITMASK_DEBUG = 0x80; + + /** + * Bitmask for the language preference field in the file header. + */ + private static final int HEADER_BITMASK_LANGUAGE_PREFERENCE = 0x10; + + /** + * Bitmask for the start position field in the file header. + */ + private static final int HEADER_BITMASK_START_POSITION = 0x40; + + /** + * Bitmask for the start zoom level field in the file header. + */ + private static final int HEADER_BITMASK_START_ZOOM_LEVEL = 0x20; + + /** + * The length of the language preference string. + */ + private static final int LANGUAGE_PREFERENCE_LENGTH = 2; + + /** + * Maximum valid start zoom level. + */ + private static final int START_ZOOM_LEVEL_MAX = 22; + + static FileOpenResult readOptionalFields(ReadBuffer readBuffer, MapFileInfoBuilder mapFileInfoBuilder) { + OptionalFields optionalFields = new OptionalFields(readBuffer.readByte()); + mapFileInfoBuilder.optionalFields = optionalFields; + + FileOpenResult fileOpenResult = optionalFields.readOptionalFields(readBuffer); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + return FileOpenResult.SUCCESS; + } + + String comment; + String createdBy; + final boolean hasComment; + final boolean hasCreatedBy; + final boolean hasLanguagePreference; + final boolean hasStartPosition; + final boolean hasStartZoomLevel; + final boolean isDebugFile; + String languagePreference; + GeoPoint startPosition; + Byte startZoomLevel; + + private OptionalFields(byte flags) { + this.isDebugFile = (flags & HEADER_BITMASK_DEBUG) != 0; + this.hasStartPosition = (flags & HEADER_BITMASK_START_POSITION) != 0; + this.hasStartZoomLevel = (flags & HEADER_BITMASK_START_ZOOM_LEVEL) != 0; + this.hasLanguagePreference = (flags & HEADER_BITMASK_LANGUAGE_PREFERENCE) != 0; + this.hasComment = (flags & HEADER_BITMASK_COMMENT) != 0; + this.hasCreatedBy = (flags & HEADER_BITMASK_CREATED_BY) != 0; + } + + private FileOpenResult readLanguagePreference(ReadBuffer readBuffer) { + if (this.hasLanguagePreference) { + String countryCode = readBuffer.readUTF8EncodedString(); + if (countryCode.length() != LANGUAGE_PREFERENCE_LENGTH) { + return new FileOpenResult("invalid language preference: " + countryCode); + } + this.languagePreference = countryCode; + } + return FileOpenResult.SUCCESS; + } + + private FileOpenResult readMapStartPosition(ReadBuffer readBuffer) { + if (this.hasStartPosition) { + // get and check the start position latitude (4 byte) + int mapStartLatitude = readBuffer.readInt(); + if (mapStartLatitude < RequiredFields.LATITUDE_MIN || mapStartLatitude > RequiredFields.LATITUDE_MAX) { + return new FileOpenResult("invalid map start latitude: " + mapStartLatitude); + } + + // get and check the start position longitude (4 byte) + int mapStartLongitude = readBuffer.readInt(); + if (mapStartLongitude < RequiredFields.LONGITUDE_MIN || mapStartLongitude > RequiredFields.LONGITUDE_MAX) { + return new FileOpenResult("invalid map start longitude: " + mapStartLongitude); + } + + this.startPosition = new GeoPoint(mapStartLatitude, mapStartLongitude); + } + return FileOpenResult.SUCCESS; + } + + private FileOpenResult readMapStartZoomLevel(ReadBuffer readBuffer) { + if (this.hasStartZoomLevel) { + // get and check the start zoom level (1 byte) + byte mapStartZoomLevel = readBuffer.readByte(); + if (mapStartZoomLevel < 0 || mapStartZoomLevel > START_ZOOM_LEVEL_MAX) { + return new FileOpenResult("invalid map start zoom level: " + mapStartZoomLevel); + } + + this.startZoomLevel = Byte.valueOf(mapStartZoomLevel); + } + return FileOpenResult.SUCCESS; + } + + private FileOpenResult readOptionalFields(ReadBuffer readBuffer) { + FileOpenResult fileOpenResult = readMapStartPosition(readBuffer); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = readMapStartZoomLevel(readBuffer); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + fileOpenResult = readLanguagePreference(readBuffer); + if (!fileOpenResult.isSuccess()) { + return fileOpenResult; + } + + if (this.hasComment) { + this.comment = readBuffer.readUTF8EncodedString(); + } + + if (this.hasCreatedBy) { + this.createdBy = readBuffer.readUTF8EncodedString(); + } + + return FileOpenResult.SUCCESS; + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/header/RequiredFields.java b/src/org/mapsforge/mapdatabase/mapfile/header/RequiredFields.java new file mode 100644 index 00000000..63dcb824 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/header/RequiredFields.java @@ -0,0 +1,235 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile.header; + +import java.io.IOException; + +import org.mapsforge.core.BoundingBox; +import org.mapsforge.core.Tag; +import org.mapsforge.mapdatabase.FileOpenResult; +import org.mapsforge.mapdatabase.mapfile.ReadBuffer; + +final class RequiredFields { + /** + * Magic byte at the beginning of a valid binary map file. + */ + private static final String BINARY_OSM_MAGIC_BYTE = "mapsforge binary OSM"; + + /** + * Maximum size of the file header in bytes. + */ + private static final int HEADER_SIZE_MAX = 1000000; + + /** + * Minimum size of the file header in bytes. + */ + private static final int HEADER_SIZE_MIN = 70; + + /** + * The name of the Mercator projection as stored in the file header. + */ + private static final String MERCATOR = "Mercator"; + + /** + * A single whitespace character. + */ + private static final char SPACE = ' '; + + /** + * Version of the map file format which is supported by this implementation. + */ + private static final int SUPPORTED_FILE_VERSION = 4; + + /** + * The maximum latitude values in microdegrees. + */ + static final int LATITUDE_MAX = 90000000; + + /** + * The minimum latitude values in microdegrees. + */ + static final int LATITUDE_MIN = -90000000; + + /** + * The maximum longitude values in microdegrees. + */ + static final int LONGITUDE_MAX = 180000000; + + /** + * The minimum longitude values in microdegrees. + */ + static final int LONGITUDE_MIN = -180000000; + + static FileOpenResult readBoundingBox(ReadBuffer readBuffer, MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the minimum latitude (4 bytes) + int minLatitude = readBuffer.readInt(); + if (minLatitude < LATITUDE_MIN || minLatitude > LATITUDE_MAX) { + return new FileOpenResult("invalid minimum latitude: " + minLatitude); + } + + // get and check the minimum longitude (4 bytes) + int minLongitude = readBuffer.readInt(); + if (minLongitude < LONGITUDE_MIN || minLongitude > LONGITUDE_MAX) { + return new FileOpenResult("invalid minimum longitude: " + minLongitude); + } + + // get and check the maximum latitude (4 bytes) + int maxLatitude = readBuffer.readInt(); + if (maxLatitude < LATITUDE_MIN || maxLatitude > LATITUDE_MAX) { + return new FileOpenResult("invalid maximum latitude: " + maxLatitude); + } + + // get and check the maximum longitude (4 bytes) + int maxLongitude = readBuffer.readInt(); + if (maxLongitude < LONGITUDE_MIN || maxLongitude > LONGITUDE_MAX) { + return new FileOpenResult("invalid maximum longitude: " + maxLongitude); + } + + // check latitude and longitude range + if (minLatitude > maxLatitude) { + return new FileOpenResult("invalid latitude range: " + minLatitude + SPACE + maxLatitude); + } else if (minLongitude > maxLongitude) { + return new FileOpenResult("invalid longitude range: " + minLongitude + SPACE + maxLongitude); + } + + mapFileInfoBuilder.boundingBox = new BoundingBox(minLatitude, minLongitude, maxLatitude, maxLongitude); + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readFileSize(ReadBuffer readBuffer, long fileSize, MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the file size (8 bytes) + long headerFileSize = readBuffer.readLong(); + if (headerFileSize != fileSize) { + return new FileOpenResult("invalid file size: " + headerFileSize); + } + mapFileInfoBuilder.fileSize = fileSize; + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readFileVersion(ReadBuffer readBuffer, MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the file version (4 bytes) + int fileVersion = readBuffer.readInt(); + if (fileVersion != SUPPORTED_FILE_VERSION) { + return new FileOpenResult("unsupported file version: " + fileVersion); + } + mapFileInfoBuilder.fileVersion = fileVersion; + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readMagicByte(ReadBuffer readBuffer) throws IOException { + // read the the magic byte and the file header size into the buffer + int magicByteLength = BINARY_OSM_MAGIC_BYTE.length(); + if (!readBuffer.readFromFile(magicByteLength + 4)) { + return new FileOpenResult("reading magic byte has failed"); + } + + // get and check the magic byte + String magicByte = readBuffer.readUTF8EncodedString(magicByteLength); + if (!BINARY_OSM_MAGIC_BYTE.equals(magicByte)) { + return new FileOpenResult("invalid magic byte: " + magicByte); + } + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readMapDate(ReadBuffer readBuffer, MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the the map date (8 bytes) + long mapDate = readBuffer.readLong(); + // is the map date before 2010-01-10 ? + if (mapDate < 1200000000000L) { + return new FileOpenResult("invalid map date: " + mapDate); + } + mapFileInfoBuilder.mapDate = mapDate; + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readPoiTags(ReadBuffer readBuffer, MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the number of POI tags (2 bytes) + int numberOfPoiTags = readBuffer.readShort(); + if (numberOfPoiTags < 0) { + return new FileOpenResult("invalid number of POI tags: " + numberOfPoiTags); + } + + Tag[] poiTags = new Tag[numberOfPoiTags]; + for (int currentTagId = 0; currentTagId < numberOfPoiTags; ++currentTagId) { + // get and check the POI tag + String tag = readBuffer.readUTF8EncodedString(); + if (tag == null) { + return new FileOpenResult("POI tag must not be null: " + currentTagId); + } + poiTags[currentTagId] = new Tag(tag); + } + mapFileInfoBuilder.poiTags = poiTags; + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readProjectionName(ReadBuffer readBuffer, MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the projection name + String projectionName = readBuffer.readUTF8EncodedString(); + if (!MERCATOR.equals(projectionName)) { + return new FileOpenResult("unsupported projection: " + projectionName); + } + mapFileInfoBuilder.projectionName = projectionName; + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readRemainingHeader(ReadBuffer readBuffer) throws IOException { + // get and check the size of the remaining file header (4 bytes) + int remainingHeaderSize = readBuffer.readInt(); + if (remainingHeaderSize < HEADER_SIZE_MIN || remainingHeaderSize > HEADER_SIZE_MAX) { + return new FileOpenResult("invalid remaining header size: " + remainingHeaderSize); + } + + // read the header data into the buffer + if (!readBuffer.readFromFile(remainingHeaderSize)) { + return new FileOpenResult("reading header data has failed: " + remainingHeaderSize); + } + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readTilePixelSize(ReadBuffer readBuffer, MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the tile pixel size (2 bytes) + int tilePixelSize = readBuffer.readShort(); + // if (tilePixelSize != Tile.TILE_SIZE) { + // return new FileOpenResult("unsupported tile pixel size: " + tilePixelSize); + // } + mapFileInfoBuilder.tilePixelSize = tilePixelSize; + return FileOpenResult.SUCCESS; + } + + static FileOpenResult readWayTags(ReadBuffer readBuffer, MapFileInfoBuilder mapFileInfoBuilder) { + // get and check the number of way tags (2 bytes) + int numberOfWayTags = readBuffer.readShort(); + if (numberOfWayTags < 0) { + return new FileOpenResult("invalid number of way tags: " + numberOfWayTags); + } + + Tag[] wayTags = new Tag[numberOfWayTags]; + + for (int currentTagId = 0; currentTagId < numberOfWayTags; ++currentTagId) { + // get and check the way tag + String tag = readBuffer.readUTF8EncodedString(); + if (tag == null) { + return new FileOpenResult("way tag must not be null: " + currentTagId); + } + wayTags[currentTagId] = new Tag(tag); + } + mapFileInfoBuilder.wayTags = wayTags; + return FileOpenResult.SUCCESS; + } + + private RequiredFields() { + throw new IllegalStateException(); + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/header/SubFileParameter.java b/src/org/mapsforge/mapdatabase/mapfile/header/SubFileParameter.java new file mode 100644 index 00000000..c16a8b65 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/header/SubFileParameter.java @@ -0,0 +1,213 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile.header; + +import org.mapsforge.core.MercatorProjection; + +/** + * Holds all parameters of a sub-file. + */ +public class SubFileParameter { + /** + * Number of bytes a single index entry consists of. + */ + public static final byte BYTES_PER_INDEX_ENTRY = 5; + + /** + * Divisor for converting coordinates stored as integers to double values. + */ + private static final double COORDINATES_DIVISOR = 1000000d; + + /** + * Base zoom level of the sub-file, which equals to one block. + */ + public final byte baseZoomLevel; + + /** + * Size of the entries table at the beginning of each block in bytes. + */ + public final int blockEntriesTableSize; + + /** + * Vertical amount of blocks in the grid. + */ + public final long blocksHeight; + + /** + * Horizontal amount of blocks in the grid. + */ + public final long blocksWidth; + + /** + * Y number of the tile at the bottom boundary in the grid. + */ + public final long boundaryTileBottom; + + /** + * X number of the tile at the left boundary in the grid. + */ + public final long boundaryTileLeft; + + /** + * X number of the tile at the right boundary in the grid. + */ + public final long boundaryTileRight; + + /** + * Y number of the tile at the top boundary in the grid. + */ + public final long boundaryTileTop; + + /** + * Absolute end address of the index in the enclosing file. + */ + public final long indexEndAddress; + + /** + * Absolute start address of the index in the enclosing file. + */ + public final long indexStartAddress; + + /** + * Total number of blocks in the grid. + */ + public final long numberOfBlocks; + + /** + * Absolute start address of the sub-file in the enclosing file. + */ + public final long startAddress; + + /** + * Size of the sub-file in bytes. + */ + public final long subFileSize; + + /** + * Maximum zoom level for which the block entries tables are made. + */ + public final byte zoomLevelMax; + + /** + * Minimum zoom level for which the block entries tables are made. + */ + public final byte zoomLevelMin; + + /** + * Stores the hash code of this object. + */ + private final int hashCodeValue; + + SubFileParameter(SubFileParameterBuilder subFileParameterBuilder) { + this.startAddress = subFileParameterBuilder.startAddress; + this.indexStartAddress = subFileParameterBuilder.indexStartAddress; + this.subFileSize = subFileParameterBuilder.subFileSize; + this.baseZoomLevel = subFileParameterBuilder.baseZoomLevel; + this.zoomLevelMin = subFileParameterBuilder.zoomLevelMin; + this.zoomLevelMax = subFileParameterBuilder.zoomLevelMax; + this.hashCodeValue = calculateHashCode(); + + // calculate the XY numbers of the boundary tiles in this sub-file + this.boundaryTileBottom = MercatorProjection.latitudeToTileY(subFileParameterBuilder.boundingBox.minLatitudeE6 + / COORDINATES_DIVISOR, this.baseZoomLevel); + this.boundaryTileLeft = MercatorProjection.longitudeToTileX(subFileParameterBuilder.boundingBox.minLongitudeE6 + / COORDINATES_DIVISOR, this.baseZoomLevel); + this.boundaryTileTop = MercatorProjection.latitudeToTileY(subFileParameterBuilder.boundingBox.maxLatitudeE6 + / COORDINATES_DIVISOR, this.baseZoomLevel); + this.boundaryTileRight = MercatorProjection.longitudeToTileX(subFileParameterBuilder.boundingBox.maxLongitudeE6 + / COORDINATES_DIVISOR, this.baseZoomLevel); + + // calculate the horizontal and vertical amount of blocks in this sub-file + this.blocksWidth = this.boundaryTileRight - this.boundaryTileLeft + 1; + this.blocksHeight = this.boundaryTileBottom - this.boundaryTileTop + 1; + + // calculate the total amount of blocks in this sub-file + this.numberOfBlocks = this.blocksWidth * this.blocksHeight; + + this.indexEndAddress = this.indexStartAddress + this.numberOfBlocks * BYTES_PER_INDEX_ENTRY; + + // calculate the size of the tile entries table + this.blockEntriesTableSize = 2 * (this.zoomLevelMax - this.zoomLevelMin + 1) * 2; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof SubFileParameter)) { + return false; + } + SubFileParameter other = (SubFileParameter) obj; + if (this.startAddress != other.startAddress) { + return false; + } else if (this.subFileSize != other.subFileSize) { + return false; + } else if (this.baseZoomLevel != other.baseZoomLevel) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return this.hashCodeValue; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("SubFileParameter [baseZoomLevel="); + stringBuilder.append(this.baseZoomLevel); + stringBuilder.append(", blockEntriesTableSize="); + stringBuilder.append(this.blockEntriesTableSize); + stringBuilder.append(", blocksHeight="); + stringBuilder.append(this.blocksHeight); + stringBuilder.append(", blocksWidth="); + stringBuilder.append(this.blocksWidth); + stringBuilder.append(", boundaryTileBottom="); + stringBuilder.append(this.boundaryTileBottom); + stringBuilder.append(", boundaryTileLeft="); + stringBuilder.append(this.boundaryTileLeft); + stringBuilder.append(", boundaryTileRight="); + stringBuilder.append(this.boundaryTileRight); + stringBuilder.append(", boundaryTileTop="); + stringBuilder.append(this.boundaryTileTop); + stringBuilder.append(", indexStartAddress="); + stringBuilder.append(this.indexStartAddress); + stringBuilder.append(", numberOfBlocks="); + stringBuilder.append(this.numberOfBlocks); + stringBuilder.append(", startAddress="); + stringBuilder.append(this.startAddress); + stringBuilder.append(", subFileSize="); + stringBuilder.append(this.subFileSize); + stringBuilder.append(", zoomLevelMax="); + stringBuilder.append(this.zoomLevelMax); + stringBuilder.append(", zoomLevelMin="); + stringBuilder.append(this.zoomLevelMin); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + + /** + * @return the hash code of this object. + */ + private int calculateHashCode() { + int result = 7; + result = 31 * result + (int) (this.startAddress ^ (this.startAddress >>> 32)); + result = 31 * result + (int) (this.subFileSize ^ (this.subFileSize >>> 32)); + result = 31 * result + this.baseZoomLevel; + return result; + } +} diff --git a/src/org/mapsforge/mapdatabase/mapfile/header/SubFileParameterBuilder.java b/src/org/mapsforge/mapdatabase/mapfile/header/SubFileParameterBuilder.java new file mode 100644 index 00000000..7996aada --- /dev/null +++ b/src/org/mapsforge/mapdatabase/mapfile/header/SubFileParameterBuilder.java @@ -0,0 +1,31 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.mapfile.header; + +import org.mapsforge.core.BoundingBox; + +class SubFileParameterBuilder { + byte baseZoomLevel; + BoundingBox boundingBox; + long indexStartAddress; + long startAddress; + long subFileSize; + byte zoomLevelMax; + byte zoomLevelMin; + + SubFileParameter build() { + return new SubFileParameter(this); + } +} diff --git a/src/org/mapsforge/mapdatabase/postgis/Geometry.java b/src/org/mapsforge/mapdatabase/postgis/Geometry.java new file mode 100644 index 00000000..68841aa7 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/postgis/Geometry.java @@ -0,0 +1,372 @@ +/* + * Geometry.java + * + * PostGIS extension for PostgreSQL JDBC driver - geometry model + * + * (C) 2004 Paul Ramsey, pramsey@refractions.net + * + * (C) 2005 Markus Schaber, markus.schaber@logix-tt.com + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General License as published by the Free + * Software Foundation, either version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General License for more + * details. + * + * You should have received a copy of the GNU Lesser General License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA or visit the web at + * http://www.gnu.org. + * + * $Id: Geometry.java 9324 2012-02-27 22:08:12Z pramsey $ + */ + +package org.mapsforge.mapdatabase.postgis; + +import java.io.Serializable; + +/** The base class of all geometries */ +abstract class Geometry implements Serializable { + /* JDK 1.5 Serialization */ + private static final long serialVersionUID = 0x100; + + // OpenGIS Geometry types as defined in the OGC WKB Spec + // (May we replace this with an ENUM as soon as JDK 1.5 + // has gained widespread usage?) + + /** Fake type for linear ring */ + static final int LINEARRING = 0; + /** + * The OGIS geometry type number for points. + */ + static final int POINT = 1; + + /** + * The OGIS geometry type number for lines. + */ + static final int LINESTRING = 2; + + /** + * The OGIS geometry type number for polygons. + */ + static final int POLYGON = 3; + + /** + * The OGIS geometry type number for aggregate points. + */ + static final int MULTIPOINT = 4; + + /** + * The OGIS geometry type number for aggregate lines. + */ + static final int MULTILINESTRING = 5; + + /** + * The OGIS geometry type number for aggregate polygons. + */ + static final int MULTIPOLYGON = 6; + + /** + * The OGIS geometry type number for feature collections. + */ + static final int GEOMETRYCOLLECTION = 7; + + static final String[] ALLTYPES = new String[] { + "", // internally used LinearRing does not have any text in front of + // it + "POINT", "LINESTRING", "POLYGON", "MULTIPOINT", "MULTILINESTRING", + "MULTIPOLYGON", "GEOMETRYCOLLECTION" }; + + /** + * The Text representations of the geometry types + * + * @param type + * ... + * @return ... + */ + static String getTypeString(int type) { + if (type >= 0 && type <= 7) + return ALLTYPES[type]; + + throw new IllegalArgumentException("Unknown Geometry type" + type); + + } + + // Properties common to all geometries + /** + * The dimensionality of this feature (2,3) + */ + int dimension; + + /** + * Do we have a measure (4th dimension) + */ + boolean haveMeasure = false; + + /** + * The OGIS geometry type of this feature. this is final as it never changes, it is bound to the subclass of the + * instance. + */ + final int type; + + /** + * Official UNKNOWN srid value + */ + final static int UNKNOWN_SRID = 0; + + /** + * The spacial reference system id of this geometry, default is no srid + */ + int srid = UNKNOWN_SRID; + + /** + * Parse a SRID value, anything <= 0 is unknown + * + * @param srid + * ... + * @return ... + */ + static int parseSRID(int srid) { + if (srid < 0) { + /* TODO: raise a warning ? */ + return 0; + } + return srid; + } + + /** + * Constructor for subclasses + * + * @param type + * has to be given by all subclasses. + */ + protected Geometry(int type) { + this.type = type; + } + + /** + * java.lang.Object hashCode implementation + */ + @Override + public int hashCode() { + return dimension | (type * 4) | (srid * 32); + } + + /** + * java.lang.Object equals implementation + */ + @Override + public boolean equals(Object other) { + return (other != null) && (other instanceof Geometry) + && equals((Geometry) other); + } + + /** + * geometry specific equals implementation - only defined for non-null values + * + * @param other + * ... + * @return ... + */ + public boolean equals(Geometry other) { + return (other != null) && (this.dimension == other.dimension) + && (this.type == other.type) && (this.srid == other.srid) + && (this.haveMeasure == other.haveMeasure) + && other.getClass().equals(this.getClass()) + && this.equalsintern(other); + } + + /** + * Whether test coordinates for geometry - subclass specific code Implementors can assume that dimensin, type, srid + * and haveMeasure are equal, other != null and other is the same subclass. + * + * @param other + * ... + * @return ... + */ + protected abstract boolean equalsintern(Geometry other); + + /** + * Return the number of Points of the geometry + * + * @return ... + */ + abstract int numPoints(); + + /** + * Get the nth Point of the geometry + * + * @param n + * the index of the point, from 0 to numPoints()-1; + * @throws ArrayIndexOutOfBoundsException + * in case of an emtpy geometry or bad index. + */ + // abstract Point getPoint(int n); + + // + // /** + // * Same as getPoint(0); + // */ + // abstract Point getFirstPoint(); + // + // /** + // * Same as getPoint(numPoints()-1); + // */ + // abstract Point getLastPoint(); + + /** + * The OGIS geometry type number of this geometry. + * + * @return ... + */ + int getType() { + return this.type; + } + + /** + * Return the Type as String + * + * @return ... + */ + String getTypeString() { + return getTypeString(this.type); + } + + /** + * Returns whether we have a measure + * + * @return .... + */ + boolean isMeasured() { + return haveMeasure; + } + + /** + * Queries the number of geometric dimensions of this geometry. This does not include measures, as opposed to the + * server. + * + * @return The dimensionality (eg, 2D or 3D) of this geometry. + */ + int getDimension() { + return this.dimension; + } + + /** + * The OGIS geometry type number of this geometry. + * + * @return ... + */ + int getSrid() { + return this.srid; + } + + /** + * Recursively sets the srid on this geometry and all contained subgeometries + * + * @param srid + * ... + */ + void setSrid(int srid) { + this.srid = srid; + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + if (srid != UNKNOWN_SRID) { + sb.append("SRID="); + sb.append(srid); + sb.append(';'); + } + outerWKT(sb, true); + return sb.toString(); + } + + /** + * Render the WKT version of this Geometry (without SRID) into the given StringBuffer. + * + * @param sb + * ... + * @param putM + * ... + */ + void outerWKT(StringBuffer sb, boolean putM) { + sb.append(getTypeString()); + if (putM && haveMeasure && dimension == 2) { + sb.append('M'); + } + mediumWKT(sb); + } + + final void outerWKT(StringBuffer sb) { + outerWKT(sb, true); + } + + /** + * Render the WKT without the type name, but including the brackets into the StringBuffer + * + * @param sb + * ... + */ + protected void mediumWKT(StringBuffer sb) { + sb.append('('); + innerWKT(sb); + sb.append(')'); + } + + /** + * Render the "inner" part of the WKT (inside the brackets) into the StringBuffer. + * + * @param SB + * ... + */ + protected abstract void innerWKT(StringBuffer SB); + + /** + * backwards compatibility method + * + * @return ... + */ + String getValue() { + StringBuffer sb = new StringBuffer(); + mediumWKT(sb); + return sb.toString(); + } + + /** + * Do some internal consistency checks on the geometry. Currently, all Geometries must have a valid dimension (2 or + * 3) and a valid type. 2-dimensional Points must have Z=0.0, as well as non-measured Points must have m=0.0. + * Composed geometries must have all equal SRID, dimensionality and measures, as well as that they do not contain + * NULL or inconsistent subgeometries. BinaryParser and WKTParser should only generate consistent geometries. + * BinaryWriter may produce invalid results on inconsistent geometries. + * + * @return true if all checks are passed. + */ + boolean checkConsistency() { + return (dimension >= 2 && dimension <= 3) && (type >= 0 && type <= 7); + } + + /** + * Splits the SRID=4711; part of a EWKT rep if present and sets the srid. + * + * @param value + * ... + * @return value without the SRID=4711; part + */ + protected String initSRID(String value) { + String v = value.trim(); + if (v.startsWith("SRID=")) { + int index = v.indexOf(';', 5); // sridprefix length is 5 + if (index == -1) { + throw new IllegalArgumentException( + "Error parsing Geometry - SRID not delimited with ';' "); + } + this.srid = Integer.parseInt(v.substring(5, index)); + return v.substring(index + 1).trim(); + } + return v; + } +} diff --git a/src/org/mapsforge/mapdatabase/postgis/MapDatabase.java b/src/org/mapsforge/mapdatabase/postgis/MapDatabase.java new file mode 100644 index 00000000..507cc255 --- /dev/null +++ b/src/org/mapsforge/mapdatabase/postgis/MapDatabase.java @@ -0,0 +1,442 @@ +/* + * Copyright 2012 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.mapsforge.mapdatabase.postgis; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.Properties; + +import org.mapsforge.core.BoundingBox; +import org.mapsforge.core.GeoPoint; +import org.mapsforge.core.SphericalMercator; +import org.mapsforge.core.Tag; +import org.mapsforge.core.Tile; +import org.mapsforge.mapdatabase.FileOpenResult; +import org.mapsforge.mapdatabase.IMapDatabase; +import org.mapsforge.mapdatabase.IMapDatabaseCallback; +import org.mapsforge.mapdatabase.MapFileInfo; +import org.postgresql.PGConnection; + +/** + * + * + */ +public class MapDatabase implements IMapDatabase { + private static final String QUERY = "SELECT * FROM __get_tile(?,?,?)"; + + private final float mScale = 1; // 1000000.0f; + + private int mCoordPos = 0; + private int mIndexPos = 0; + private float[] mCoords = new float[100000]; + private int[] mIndex = new int[10000]; + + private Tag[] mTags; + + private final MapFileInfo mMapInfo = + new MapFileInfo(new BoundingBox(-180, -85, 180, 85), + new Byte((byte) 14), new GeoPoint(53.11, 8.85), SphericalMercator.NAME, + 0, 0, 0, "de", "yo!", "hannes"); + // new MapFileInfo(new BoundingBox(-180, -90, 180, 90), + // new Byte((byte) 0), null, "Mercator", + // 0, 0, 0, "de", "yo!", "by me"); + + private boolean mOpenFile = false; + + private Connection connection = null; + private static HashMap, Tag> tagHash = new HashMap, Tag>(100); + private PreparedStatement prepQuery = null; + + private boolean connect() { + Connection conn = null; + // &socketTimeout=15&tcpKeepAlive=true + + String dburl = "jdbc:postgresql://city.informatik.uni-bremen.de:5432/gis"; + // String dburl = "jdbc:postgresql://city.informatik.uni-bremen.de:5432/planet-2.0"; + // String dburl = "jdbc:postgresql://127.0.0.1:5432/bremen"; + // String dburl = "jdbc:postgresql://127.0.0.1:5431/planet-2.0"; + + Properties dbOpts = new Properties(); + dbOpts.setProperty("user", "osm"); + dbOpts.setProperty("password", "osm"); + dbOpts.setProperty("socketTimeout", "15000"); + dbOpts.setProperty("tcpKeepAlive", "true"); + + try { + DriverManager.setLoginTimeout(20); + System.out.println("Creating JDBC connection..."); + Class.forName("org.postgresql.Driver"); + conn = DriverManager.getConnection(dburl, dbOpts); + connection = conn; + prepQuery = conn.prepareStatement(QUERY); + + PGConnection pgconn = (PGConnection) conn; + + pgconn.addDataType("hstore", PGHStore.class); + + } catch (Exception e) { + System.err.println("Aborted due to error:"); + e.printStackTrace(); + return false; + } + return true; + } + + @Override + public void executeQuery(Tile tile, IMapDatabaseCallback mapDatabaseCallback) { + if (connection == null) { + if (!connect()) + return; + } + + ResultSet r; + + try { + prepQuery.setLong(1, tile.pixelX); + prepQuery.setLong(2, tile.pixelY); + prepQuery.setInt(3, tile.zoomLevel); + prepQuery.execute(); + r = prepQuery.getResultSet(); + } catch (SQLException e) { + e.printStackTrace(); + connection = null; + return; + } + + byte[] b = null; + PGHStore h = null; + // long id; + + try { + while (r != null && r.next()) { + mIndexPos = 0; + mCoordPos = 0; + + try { + // id = r.getLong(1); + + Object obj = r.getObject(2); + h = null; + + if (obj instanceof PGHStore) + h = (PGHStore) obj; + else + continue; + + b = r.getBytes(3); + + } catch (SQLException e) { + e.printStackTrace(); + continue; + } + + if (b == null) + continue; + + mTags = new Tag[h.size()]; + + int i = 0; + for (Entry t : h.entrySet()) { + if (t.getKey() == null) { + System.out.println("no KEY !!! "); + break; + } + Tag tag = tagHash.get(t); + if (tag == null) { + tag = new Tag(t.getKey(), t.getValue()); + tagHash.put(t, tag); + + } + mTags[i++] = tag; + } + if (i < mTags.length) + continue; + + parse(b); + if (mIndexPos == 0) + continue; + + int[] idx = new int[mIndexPos]; + System.arraycopy(mIndex, 0, idx, 0, mIndexPos); + mapDatabaseCallback.renderWay((byte) 0, mTags, mCoords, idx, true); + } + } catch (SQLException e) { + e.printStackTrace(); + + try { + connection.close(); + } catch (SQLException e1) { + e1.printStackTrace(); + } finally { + connection = null; + } + } + + } + + @Override + public MapFileInfo getMapFileInfo() { + return mMapInfo; + } + + @Override + public boolean hasOpenFile() { + return mOpenFile; + } + + @Override + public FileOpenResult openFile(File mapFile) { + mOpenFile = true; + return new FileOpenResult(); + } + + @Override + public void closeFile() { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e1) { + e1.printStackTrace(); + } finally { + connection = null; + } + } + mOpenFile = false; + } + + @Override + public String readString(int position) { + return null; + } + + // taken from postgis-java + + private static ValueGetter valueGetterForEndian(byte[] bytes) { + if (bytes[0] == ValueGetter.XDR.NUMBER) { // XDR + return new ValueGetter.XDR(bytes); + } else if (bytes[0] == ValueGetter.NDR.NUMBER) { + return new ValueGetter.NDR(bytes); + } else { + throw new IllegalArgumentException("Unknown Endian type:" + bytes[0]); + } + } + + /** + * Parse a binary encoded geometry. Is synchronized to protect offset counter. (Unfortunately, Java does not have + * neither call by reference nor multiple return values.) + * + * @param value + * ... + */ + private void parse(byte[] value) { + parseGeometry(valueGetterForEndian(value)); + } + + private void parseGeometry(ValueGetter data) { + byte endian = data.getByte(); // skip and test endian flag + if (endian != data.endian) { + throw new IllegalArgumentException("Endian inconsistency!"); + } + int typeword = data.getInt(); + + int realtype = typeword & 0x1FFFFFFF; // cut off high flag bits + + boolean haveZ = (typeword & 0x80000000) != 0; + boolean haveM = (typeword & 0x40000000) != 0; + boolean haveS = (typeword & 0x20000000) != 0; + + // int srid = Geometry.UNKNOWN_SRID; + + if (haveS) { + // srid = Geometry.parseSRID(data.getInt()); + data.getInt(); + } + switch (realtype) { + case Geometry.POINT: + parsePoint(data, haveZ, haveM); + break; + case Geometry.LINESTRING: + parseLineString(data, haveZ, haveM); + break; + case Geometry.POLYGON: + parsePolygon(data, haveZ, haveM); + break; + case Geometry.MULTIPOINT: + parseMultiPoint(data); + break; + case Geometry.MULTILINESTRING: + parseMultiLineString(data); + break; + case Geometry.MULTIPOLYGON: + parseMultiPolygon(data); + break; + case Geometry.GEOMETRYCOLLECTION: + parseCollection(data); + break; + default: + throw new IllegalArgumentException("Unknown Geometry Type: " + realtype); + } + // if (srid != Geometry.UNKNOWN_SRID) { + // result.setSrid(srid); + // } + } + + private static void parsePoint(ValueGetter data, boolean haveZ, boolean haveM) { + // double X = data.getDouble(); + // double Y = data.getDouble(); + data.getDouble(); + data.getDouble(); + + if (haveZ) + data.getDouble(); + + if (haveM) + data.getDouble(); + + } + + /** + * Parse an Array of "full" Geometries + * + * @param data + * ... + * @param count + * ... + */ + private void parseGeometryArray(ValueGetter data, int count) { + for (int i = 0; i < count; i++) { + parseGeometry(data); + } + } + + // + // private void parsePointArray(ValueGetter data, boolean haveZ, boolean haveM) { + // int count = data.getInt(); + // for (int i = 0; i < count; i++) { + // parsePoint(data, haveZ, haveM); + // } + // } + + private void parseMultiPoint(ValueGetter data) { + parseGeometryArray(data, data.getInt()); + } + + private void parseLineString(ValueGetter data, boolean haveZ, boolean haveM) { + int count = data.getInt(); + for (int i = 0; i < count; i++) { + mCoords[mCoordPos++] = (float) (data.getDouble()) * mScale; + mCoords[mCoordPos++] = (float) (data.getDouble()) * mScale; + if (haveZ) + data.getDouble(); + if (haveM) + data.getDouble(); + } + mIndex[mIndexPos++] = count * 2; + } + + private void parsePolygon(ValueGetter data, boolean haveZ, boolean haveM) { + int count = data.getInt(); + + for (int i = 0; i < count; i++) { + parseLineString(data, haveZ, haveM); + } + } + + private void parseMultiLineString(ValueGetter data) { + int count = data.getInt(); + parseGeometryArray(data, count); + } + + private void parseMultiPolygon(ValueGetter data) { + int count = data.getInt(); + parseGeometryArray(data, count); + } + + private void parseCollection(ValueGetter data) { + int count = data.getInt(); + parseGeometryArray(data, count); + } + + // taken from mapsforge GeoUtils + // private static final double EQUATORIAL_RADIUS = 6378137.0; + // private static final double[] EPSILON_ZERO = new double[] { 0, 0 }; + // + // static double latitudeDistance(int meters) { + // return (meters * 360) / (2 * Math.PI * EQUATORIAL_RADIUS); + // } + // + // static double longitudeDistance(int meters, double latitude) { + // return (meters * 360) / (2 * Math.PI * EQUATORIAL_RADIUS * Math.cos(Math.toRadians(latitude))); + // } + // + // private static double[] bufferInDegrees(long tileY, byte zoom, int enlargementInMeter) { + // if (enlargementInMeter == 0) { + // return EPSILON_ZERO; + // } + // + // double[] epsilons = new double[2]; + // double lat = MercatorProjection.tileYToLatitude(tileY, zoom); + // epsilons[0] = latitudeDistance(enlargementInMeter); + // epsilons[1] = longitudeDistance(enlargementInMeter, lat); + // + // return epsilons; + // } + // + // static String tileToBOX3D(Tile tile, int pixel) { + // double minLat = MercatorProjection.pixelYToLatitude(tile.getPixelY() + Tile.TILE_SIZE + pixel, tile.zoomLevel); + // double maxLat = MercatorProjection.pixelYToLatitude(tile.getPixelY() - pixel, tile.zoomLevel); + // + // double minLon = MercatorProjection.pixelXToLongitude(tile.getPixelX() - pixel, tile.zoomLevel); + // double maxLon = MercatorProjection.pixelXToLongitude(tile.getPixelX() + Tile.TILE_SIZE + pixel, tile.zoomLevel); + // + // return "ST_SetSRID('BOX3D(" + minLon + " " + minLat + ", " + " " + maxLon + " " + maxLat + ")'::box3d ,4326)"; + // } + // + // static String tileToBOX3D(Tile tile, int pixel, int projection) { + // + // double minLat = MercatorProjection.pixelYToLatitude(tile.getPixelY() + Tile.TILE_SIZE + pixel, tile.zoomLevel); + // double maxLat = MercatorProjection.pixelYToLatitude(tile.getPixelY() - pixel, tile.zoomLevel); + // + // double minLon = MercatorProjection.pixelXToLongitude(tile.getPixelX() - pixel, tile.zoomLevel); + // double maxLon = MercatorProjection.pixelXToLongitude(tile.getPixelX() + Tile.TILE_SIZE + pixel, tile.zoomLevel); + // + // return String + // .format("ST_Transform(ST_SetSRID('BOX3D(%f %f, %f %f)'::box3d , 4326), %d)", + // new Double(minLon), new Double(minLat), new Double(maxLon), new Double(maxLat), new Integer( + // projection)); + // } + // + // static String tileToBOX3D(long tileX, long tileY, byte zoom, int enlargementInMeter) { + // double minLat = MercatorProjection.tileYToLatitude(tileY + 1, zoom); + // double maxLat = MercatorProjection.tileYToLatitude(tileY, zoom); + // double minLon = MercatorProjection.tileXToLongitude(tileX, zoom); + // double maxLon = MercatorProjection.tileXToLongitude(tileX + 1, zoom); + // + // double[] epsilons = bufferInDegrees(tileY, zoom, enlargementInMeter); + // + // minLon -= epsilons[1]; + // minLat -= epsilons[0]; + // maxLon += epsilons[1]; + // maxLat += epsilons[0]; + // + // return "ST_SetSRID('BOX3D(" + minLon + " " + minLat + ", " + " " + maxLon + " " + maxLat + ")'::box3d ,4326)"; + // } +} diff --git a/src/org/mapsforge/mapdatabase/postgis/PGHStore.java b/src/org/mapsforge/mapdatabase/postgis/PGHStore.java new file mode 100644 index 00000000..015aac4a --- /dev/null +++ b/src/org/mapsforge/mapdatabase/postgis/PGHStore.java @@ -0,0 +1,416 @@ +/* + * This file has been copied from the following location: + * http://archives.postgresql.org/pgsql-jdbc/2009-12/msg00037.php + * + * PostgreSQL code is typically under a BSD licence. + * http://jdbc.postgresql.org/license.html + */ + +/*------------------------------------------------------------------------- + * + * A preliminary version of a custom type wrapper for hstore data. + * Once it gets some testing and cleanups it will go into the official + * PG JDBC driver, but stick it here for now because we need it sooner. + * + * Copyright (c) 2009, PostgreSQL Global Development Group + * + * IDENTIFICATION + * $PostgreSQL$ + * + *------------------------------------------------------------------------- + */ +package org.mapsforge.mapdatabase.postgis; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.postgresql.util.PGobject; + +/** + * This implements a class that handles the PostgreSQL contrib/hstore type + */ +public class PGHStore extends PGobject implements Map +{ + private final static long serialVersionUID = 1; + private Map _map; + + /** + * required by the driver + */ + public PGHStore() + { + setType("hstore"); + _map = new HashMap(); + } + + /** + * Initialize a hstore with a given string representation + * + * @param value + * String representated hstore + * @throws SQLException + * Is thrown if the string representation has an unknown format + * @see #setValue(String) + */ + public PGHStore(String value) + throws SQLException + { + this(); + setValue(value); + } + + /** + * @param map + * ... + */ + public PGHStore(Map map) + { + this(); + setValue(map); + } + + /** + * @param map + * ... + */ + public void setValue(Map map) + { + _map = map; + } + + /** + */ + @Override + public void setValue(String value) + throws SQLException + { + Parser p = new Parser(); + _map = p.parse(value); + } + + /** + * Returns the stored information as a string + * + * @return String represented hstore + */ + @Override + public String getValue() + { + StringBuffer buf = new StringBuffer(); + Iterator i = _map.keySet().iterator(); + boolean first = true; + while (i.hasNext()) { + Object key = i.next(); + Object val = _map.get(key); + + if (first) { + first = false; + } else { + buf.append(','); + } + + writeValue(buf, key); + buf.append("=>"); + writeValue(buf, val); + } + + return buf.toString(); + } + + private static void writeValue(StringBuffer buf, Object o) { + if (o == null) { + buf.append("NULL"); + return; + } + + String s = o.toString(); + + buf.append('"'); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '"' || c == '\\') { + buf.append('\\'); + } + buf.append(c); + } + buf.append('"'); + } + + /** + * Returns whether an object is equal to this one or not + * + * @param obj + * Object to compare with + * @return true if the two hstores are identical + */ + @Override + public boolean equals(Object obj) + { + if (obj == null) + return false; + + if (obj == this) + return true; + + if (!(obj instanceof PGHStore)) + return false; + + return _map.equals(((PGHStore) obj)._map); + + } + + private static class Parser { + private String value; + private int ptr; + private StringBuffer cur; + private boolean escaped; + + private List keys; + private List values; + + private final static int GV_WAITVAL = 0; + private final static int GV_INVAL = 1; + private final static int GV_INESCVAL = 2; + private final static int GV_WAITESCIN = 3; + private final static int GV_WAITESCESCIN = 4; + + private final static int WKEY = 0; + private final static int WVAL = 1; + private final static int WEQ = 2; + private final static int WGT = 3; + private final static int WDEL = 4; + + public Parser() { + } + + Map parse(String val) throws SQLException { + this.value = val; + ptr = 0; + keys = new ArrayList(); + values = new ArrayList(); + + parseHStore(); + + Map map = new HashMap(); + for (int i = 0; i < keys.size(); i++) { + map.put(keys.get(i), values.get(i)); + } + + return map; + } + + private boolean getValue(boolean ignoreEqual) throws SQLException { + int state = GV_WAITVAL; + + cur = new StringBuffer(); + escaped = false; + + while (true) { + boolean atEnd = (value.length() == ptr); + char c = '\0'; + if (!atEnd) { + c = value.charAt(ptr); + } + + if (state == GV_WAITVAL) { + if (c == '"') { + escaped = true; + state = GV_INESCVAL; + } else if (c == '\0') { + return false; + } else if (c == '=' && !ignoreEqual) { + throw new SQLException("KJJ"); + } else if (c == '\\') { + state = GV_WAITESCIN; + } else if (!Character.isWhitespace(c)) { + cur.append(c); + state = GV_INVAL; + } + } else if (state == GV_INVAL) { + if (c == '\\') { + state = GV_WAITESCIN; + } else if (c == '=' && !ignoreEqual) { + ptr--; + return true; + } else if (c == ',' && ignoreEqual) { + ptr--; + return true; + } else if (Character.isWhitespace(c)) { + return true; + } else if (c == '\0') { + ptr--; + return true; + } else { + cur.append(c); + } + } else if (state == GV_INESCVAL) { + if (c == '\\') { + state = GV_WAITESCESCIN; + } else if (c == '"') { + return true; + } else if (c == '\0') { + throw new SQLException("KJJ, unexpected end of string"); + } else { + cur.append(c); + } + } else if (state == GV_WAITESCIN) { + if (c == '\0') { + throw new SQLException("KJJ, unexpected end of string"); + } + + cur.append(c); + state = GV_INVAL; + } else if (state == GV_WAITESCESCIN) { + if (c == '\0') { + throw new SQLException("KJJ, unexpected end of string"); + } + + cur.append(c); + state = GV_INESCVAL; + } else { + throw new SQLException("KJJ"); + } + + ptr++; + } + } + + private void parseHStore() throws SQLException { + int state = WKEY; + escaped = false; + + while (true) { + char c = '\0'; + if (ptr < value.length()) { + c = value.charAt(ptr); + } + + if (state == WKEY) { + if (!getValue(false)) + return; + + keys.add(cur.toString()); + cur = null; + state = WEQ; + } else if (state == WEQ) { + if (c == '=') { + state = WGT; + } else if (state == '\0') { + throw new SQLException("KJJ, unexpected end of string"); + } else if (!Character.isWhitespace(c)) { + throw new SQLException("KJJ, syntax err"); + } + } else if (state == WGT) { + if (c == '>') { + state = WVAL; + } else if (c == '\0') { + throw new SQLException("KJJ, unexpected end of string"); + } else { + throw new SQLException("KJJ, syntax err [" + c + "] at " + ptr); + } + } else if (state == WVAL) { + if (!getValue(true)) { + throw new SQLException("KJJ, unexpected end of string"); + } + + String val = cur.toString(); + cur = null; + if (!escaped && "null".equalsIgnoreCase(val)) { + val = null; + } + + values.add(val); + state = WDEL; + } else if (state == WDEL) { + if (c == ',') + { + state = WKEY; + } else if (c == '\0') { + return; + } else if (!Character.isWhitespace(c)) { + throw new SQLException("KJJ, syntax err"); + } + } else { + throw new SQLException("KJJ unknown state"); + } + + ptr++; + } + } + + } + + // Farm out all the work to the real underlying map. + + @Override + public void clear() { + _map.clear(); + } + + @Override + public boolean containsKey(Object key) { + return _map.containsKey(key); + } + + @Override + public boolean containsValue(Object val) { + return _map.containsValue(val); + } + + @Override + public Set> entrySet() { + return _map.entrySet(); + } + + @Override + public String get(Object key) { + return _map.get(key); + } + + @Override + public int hashCode() { + return _map.hashCode(); + } + + @Override + public boolean isEmpty() { + return _map.isEmpty(); + } + + @Override + public Set keySet() { + return _map.keySet(); + } + + @Override + public String put(String key, String val) { + return _map.put(key, val); + } + + @Override + public void putAll(Map m) { + _map.putAll(m); + } + + @Override + public String remove(Object key) { + return _map.remove(key); + } + + @Override + public int size() { + return _map.size(); + } + + @Override + public Collection values() { + return _map.values(); + } + +} diff --git a/src/org/mapsforge/mapdatabase/postgis/ValueGetter.java b/src/org/mapsforge/mapdatabase/postgis/ValueGetter.java new file mode 100644 index 00000000..f16fe58d --- /dev/null +++ b/src/org/mapsforge/mapdatabase/postgis/ValueGetter.java @@ -0,0 +1,132 @@ +/* + * ValueGetter.java + * + * PostGIS extension for PostgreSQL JDBC driver - Binary Parser + * + * (C) 2005 Markus Schaber, markus.schaber@logix-tt.com + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General License as published by the Free + * Software Foundation, either version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General License for more + * details. + * + * You should have received a copy of the GNU Lesser General License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA or visit the web at + * http://www.gnu.org. + * + * $Id: ValueGetter.java 9324 2012-02-27 22:08:12Z pramsey $ + */ + +package org.mapsforge.mapdatabase.postgis; + +abstract class ValueGetter { + byte[] data; + int position; + final byte endian; + + ValueGetter(byte[] data, byte endian) { + this.data = data; + + this.endian = endian; + } + + /** + * Get a byte, should be equal for all endians + * + * @return ... + */ + byte getByte() { + return data[position++]; + } + + int getInt() { + int res = getInt(position); + position += 4; + return res; + } + + long getLong() { + long res = getLong(position); + position += 8; + return res; + } + + /** + * Get a 32-Bit integer + * + * @param index + * ... + * @return ... + */ + protected abstract int getInt(int index); + + /** + * Get a long value. This is not needed directly, but as a nice side-effect from GetDouble. + * + * @param index + * ... + * @return ... + */ + protected abstract long getLong(int index); + + /** + * Get a double. + * + * @return ... + */ + double getDouble() { + long bitrep = getLong(); + return Double.longBitsToDouble(bitrep); + } + + static class XDR extends ValueGetter { + static final byte NUMBER = 0; + + XDR(byte[] data) { + super(data, NUMBER); + } + + @Override + protected int getInt(int index) { + return ((data[index] & 0xFF) << 24) + ((data[index + 1] & 0xFF) << 16) + + ((data[index + 2] & 0xFF) << 8) + (data[index + 3] & 0xFF); + } + + @Override + protected long getLong(int index) { + + return ((long) (data[index] & 0xFF) << 56) | ((long) (data[index + 1] & 0xFF) << 48) + | ((long) (data[index + 2] & 0xFF) << 40) | ((long) (data[index + 3] & 0xFF) << 32) + | ((long) (data[index + 4] & 0xFF) << 24) | ((long) (data[index + 5] & 0xFF) << 16) + | ((long) (data[index + 6] & 0xFF) << 8) | ((long) (data[index + 7] & 0xFF) << 0); + } + } + + static class NDR extends ValueGetter { + static final byte NUMBER = 1; + + NDR(byte[] data) { + super(data, NUMBER); + } + + @Override + protected int getInt(int index) { + return ((data[index + 3] & 0xFF) << 24) + ((data[index + 2] & 0xFF) << 16) + + ((data[index + 1] & 0xFF) << 8) + (data[index] & 0xFF); + } + + @Override + protected long getLong(int index) { + return ((long) (data[index + 7] & 0xFF) << 56) | ((long) (data[index + 6] & 0xFF) << 48) + | ((long) (data[index + 5] & 0xFF) << 40) | ((long) (data[index + 4] & 0xFF) << 32) + | ((long) (data[index + 3] & 0xFF) << 24) | ((long) (data[index + 2] & 0xFF) << 16) + | ((long) (data[index + 1] & 0xFF) << 8) | ((long) (data[index] & 0xFF) << 0); + + } + } +}