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