diff --git a/README.md b/README.md
index f3c78df..1dcb519 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,40 @@
# osm-focus
-OSMFocus Android application
+OSMFocus Android application - OpenStreetMap Data for Android
+
+![Image](images/featuregfx.png?raw=true)
+
+OSMfocus can show details of nearby objects from the OpenStreetMap database such
+that they can be compared with real world observations. Correcting errors or
+adding missing information to OpenStreetMap and thus getting it 'into focus' is
+the main purpose of OSMfocus. OSMfocus is not a map, navigation tool or
+OpenStreetMap editor.
+
+Discrepancies between the real-world and OpenStreetMap are best observed on-site
+while memorizing details for later comparison with OpenStreetMap is usually
+difficult and with a good likelihood of missing the actual differences.
+
+![Image](images/screen01.png?raw=true)
+![Image](images/screen02.png?raw=true)
+![Image](images/screen03.png?raw=true)
+![Image](images/screen04.png?raw=true)
+
+OSMfocus shows key-value pairs as = and abbreviate two pairs that occur quite
+often, namely highway= and name= which are shown as :. Way objects which has
+just a single key-value pair of type 'building=*' are ignored when searching for
+nearby objects. To make better utilization of screen space, the following keys
+are not shown: 'kms:*', 'osak:*', 'created_by', 'addr:country', 'addr:postcode'
+and 'source'.
+
+OSMfocus use location services and network access permissions for downloading
+vector map data and background map tiles for your current location.
+
+OSMfocus use data from OpenStreetMap (www.openstreetmap.org). This data and
+screenshots containing maps are (C) Copyright OpenSteetMap contributors.
+
+[OSMfocus on Google play](https://play.google.com/store/apps/details?id=dk.network42.osmfocus)
+
+# About the Name OSMfocus
+
+The thinking behind the application name is that the application will help
+"sharpening" the OpenStreetMap data such that it represents a more clear view of
+the observable world.
\ No newline at end of file
diff --git a/images/featuregfx.png b/images/featuregfx.png
new file mode 100644
index 0000000..22aced5
Binary files /dev/null and b/images/featuregfx.png differ
diff --git a/images/icon.png b/images/icon.png
new file mode 100644
index 0000000..f825e10
Binary files /dev/null and b/images/icon.png differ
diff --git a/images/screen01.png b/images/screen01.png
new file mode 100644
index 0000000..3afebdf
Binary files /dev/null and b/images/screen01.png differ
diff --git a/images/screen02.png b/images/screen02.png
new file mode 100644
index 0000000..8aa8e9d
Binary files /dev/null and b/images/screen02.png differ
diff --git a/images/screen03.png b/images/screen03.png
new file mode 100644
index 0000000..7892859
Binary files /dev/null and b/images/screen03.png differ
diff --git a/images/screen04.png b/images/screen04.png
new file mode 100644
index 0000000..b2a82b0
Binary files /dev/null and b/images/screen04.png differ
diff --git a/res/layout/activity_main.xml b/res/layout/activity_main.xml
new file mode 100644
index 0000000..1e10b62
--- /dev/null
+++ b/res/layout/activity_main.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/res/menu/main.xml b/res/menu/main.xml
new file mode 100644
index 0000000..7685e26
--- /dev/null
+++ b/res/menu/main.xml
@@ -0,0 +1,13 @@
+
diff --git a/res/menu/main_activity_actions.xml b/res/menu/main_activity_actions.xml
new file mode 100644
index 0000000..d8d3bb5
--- /dev/null
+++ b/res/menu/main_activity_actions.xml
@@ -0,0 +1,15 @@
+
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..5a00160
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+ - Tiny
+ - Small
+ - Medium
+ - Medium+
+ - Large
+ - Huge
+
+
+
+ - 8
+ - 10
+ - 11
+ - 12
+ - 14
+ - 18
+
+
+
+ - Auto
+ - 2 (top of screen)
+ - 4
+ - 8
+
+
+
+ - 0
+ - 2
+ - 4
+ - 8
+
+
+
+ - None
+ - OSM Tiles
+ - OSM Cyclemap Tiles
+ - Internal Vector (experimental)
+
+
+
+ - 1
+ - 2
+ - 3
+ - 4
+
+
+
+ - Manual
+ - Automatic/Small
+ - Automatic/Large
+
+
+
+ - 1
+ - 2
+ - 3
+
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..bccadf7
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,7 @@
+
+
+
+ 16dp
+ 16dp
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..25abde1
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,39 @@
+
+
+
+ OSMfocus
+ Settings
+ Download
+ TestDownload
+ Where am I?
+ Toggle debug HUD
+ Load From Cache
+
+ No map data loaded.
+ Press and hold to open menu.
+ Enabled
+ Disabled
+ Waiting for location
+ Location outside downloaded area
+ Downloading area
+ Could not download area!
+ POIs not shown at high zoom levels
+ Vector data not shown at high zoom levels
+
+ Auto Download
+ Enable automatic download of map data
+
+ Wireframe Map
+ Show POI lines
+
+ Number of POIs on screen
+ Number of POIs on screen
+
+ POI Label Size
+ Size of font used for element labels
+
+ Background Map Type
+ Type of background map to show
+
+ © OpenStreetMap contributors
+
diff --git a/res/values/styles.xml b/res/values/styles.xml
new file mode 100644
index 0000000..13a7798
--- /dev/null
+++ b/res/values/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
new file mode 100644
index 0000000..e5fdca2
--- /dev/null
+++ b/res/xml/preferences.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/dk/network42/osmfocus/CustomExceptionHandler.java b/src/dk/network42/osmfocus/CustomExceptionHandler.java
new file mode 100644
index 0000000..503ec54
--- /dev/null
+++ b/src/dk/network42/osmfocus/CustomExceptionHandler.java
@@ -0,0 +1,63 @@
+package dk.network42.osmfocus;
+
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.sql.Timestamp;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Log;
+
+public class CustomExceptionHandler implements UncaughtExceptionHandler {
+ private static String TAG = "CustomExceptionHandler";
+
+ private UncaughtExceptionHandler mDefaultHandler;
+ private SharedData mG;
+ private String mUrl;
+
+ public CustomExceptionHandler(SharedData g, String url) {
+ mG = g;
+ mUrl = url;
+ mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ public void uncaughtException(Thread t, Throwable e) {
+ Log.e(TAG, "Exception:"+e);
+ Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+ final Writer result = new StringWriter();
+ final PrintWriter printWriter = new PrintWriter(result);
+ e.printStackTrace(printWriter);
+ mG.printState(printWriter);
+ String data = result.toString();
+ printWriter.close();
+ String filename = timestamp + ".txt";
+
+ writeFile(filename, data);
+
+ mDefaultHandler.uncaughtException(t, e);
+ }
+
+ private void writeFile(String filename, String data) {
+ String state = Environment.getExternalStorageState();
+ if ( ! Environment.MEDIA_MOUNTED.equals(state)) {
+ Log.e(TAG, "External media not available");
+ }
+ String filenm = mG.mCtx.getExternalFilesDir(null)+ "/" + filename;
+ Log.e(TAG, "Writing crash data to file '"+filenm+"' = "+data);
+ try {
+ FileOutputStream os = new FileOutputStream(filenm);
+ os.write(data.getBytes());
+ os.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Error writing " + filenm, e);
+ }
+ Log.e(TAG, "Done writing crash data to file");
+ }
+}
diff --git a/src/dk/network42/osmfocus/Filter.java b/src/dk/network42/osmfocus/Filter.java
new file mode 100644
index 0000000..3277464
--- /dev/null
+++ b/src/dk/network42/osmfocus/Filter.java
@@ -0,0 +1,15 @@
+package dk.network42.osmfocus;
+
+public class Filter {
+ public void filter(OsmElement e) {
+// if (e.getTagCnt() == 21) { // FIXME
+// e.mFiltered = true;
+// }
+ if (e.getTagCnt() == 1 && e.hasTag("building", "yes")) { // FIXME
+ e.mFiltered = true;
+ }
+// if (e.hasTag("landuse", "residential")) { // FIXME
+// e.mFiltered = true;
+// }
+ }
+}
diff --git a/src/dk/network42/osmfocus/GeoBBox.java b/src/dk/network42/osmfocus/GeoBBox.java
new file mode 100644
index 0000000..7babf27
--- /dev/null
+++ b/src/dk/network42/osmfocus/GeoBBox.java
@@ -0,0 +1,45 @@
+package dk.network42.osmfocus;
+
+import android.util.Log;
+
+// WGS84 based bounding box
+public class GeoBBox {
+ private static final String TAG = "GeoBBox";
+
+ // primary attributes, multiplied by 1E7 (OSM style), all floats are derived from these
+ public int left, right; // left <= right
+ public int bottom, top; // bottom <= top
+ public boolean empty = true;
+
+ public void check() {
+ assert(left <= right);
+ assert(bottom <= top);
+ }
+
+ public GeoBBox(final int left, final int bottom, final int right, final int top) {
+ this.left = left;
+ this.bottom = bottom;
+ this.right = right;
+ this.top = top;
+ this.empty = false;
+ check();
+ }
+
+ public GeoBBox(final double left, final double bottom, final double right, final double top) {
+ this((int)(left*1E7), (int)(bottom*1E7), (int)(right*1E7), (int)(top*1E7));
+ }
+
+ public String toString() {
+ return "["+left/1E7+","+bottom/1E7+"--"+right/1E7+","+top/1E7+"]";
+ }
+
+ // Get BBox centered at point with given size. Size ('meters') measured to both sides of central
+ // point, i.e. box will be 2*meters wide
+ public static GeoBBox getBoxForPoint(final double lat, final double lon, final double meters) {
+ double dax = GeoMath.convertMetersToGeoDistancePar(meters, lat);
+ double day = GeoMath.convertMetersToGeoDistanceMed(meters);
+ GeoBBox box = new GeoBBox(lon-dax, lat-day, lon+dax, lat+day);
+ //Log.d(TAG, "GeoBBox for point ("+lat+","+lon+") size="+meters+" -> deltaAngles="+dax+","+day+", box="+box);
+ return box;
+ }
+}
diff --git a/src/dk/network42/osmfocus/GeoMath.java b/src/dk/network42/osmfocus/GeoMath.java
new file mode 100644
index 0000000..35f07ef
--- /dev/null
+++ b/src/dk/network42/osmfocus/GeoMath.java
@@ -0,0 +1,120 @@
+package dk.network42.osmfocus;
+
+public class GeoMath {
+
+ // Convenience constants
+ private static final double _180_PI = 180.0/Math.PI;
+ private static final double _PI_2 = Math.PI/2.0;
+ private static final double _PI_4 = Math.PI/4.0;
+ private static final double _PI_180 = Math.PI/180.0;
+ private static final double _PI_360 = Math.PI/360.0;
+
+ // Maximum latitude due to Mercator projection.
+ public static final double MAX_LAT = _180_PI * Math.atan(Math.sinh(Math.PI));
+
+ // See http://en.wikipedia.org/wiki/Earth_ellipsoid
+ public static final double EARTH_POLAR_RADIUS = 6356752;
+ public static final double EARTH_EQUATORIAL_RADIUS = 6378137;
+ // Reference Earth radius in meters (mean between equatorial and polar WGS84 radii).
+ public static final double EARTH_RADIUS = (EARTH_EQUATORIAL_RADIUS+EARTH_POLAR_RADIUS)/2;
+
+ // Approximate conversions between 'on the ground' meter measure to angle [degrees]
+ // One equator
+ public static double convertMetersToGeoDistanceEq(double meters) {
+ return _180_PI*meters/EARTH_EQUATORIAL_RADIUS;
+ }
+ // On parallel at a given latitude
+ public static double convertMetersToGeoDistancePar(double meters, double lat) {
+ return _180_PI*meters/EARTH_EQUATORIAL_RADIUS/Math.cos(lat*_PI_180);
+ }
+ // On median
+ public static double convertMetersToGeoDistanceMed(double meters) {
+ return _180_PI*meters/EARTH_POLAR_RADIUS;
+ }
+
+ // Mercator projection. Truncated to MAX_LAT if lat is outside +-MAX_LAT.
+ // http://en.wikipedia.org/wiki/Mercator_projection#Mathematics_of_the_projection
+ public static double latToMercator(double lat) {
+ lat = Math.min(MAX_LAT, lat);
+ lat = Math.max(-MAX_LAT, lat);
+ return _180_PI * Math.log(Math.tan(lat*_PI_360 + _PI_4));
+ }
+
+ // Inverse mercator projection
+ public static double mercatorToLat(double merc) {
+ return _180_PI * (2.0 * Math.atan(Math.exp(merc*_PI_180)) - _PI_2);
+ }
+
+ public static double getMercatorScale(double lat) {
+ return latToMercator(lat)/lat;
+ }
+
+ // Closest point on finite line (l1 to l2) from point (p)
+ // Algorithm, see http://paulbourke.net/geometry/pointlineplane/
+ public static double[] nearestPoint(double px, double py, double l1x, double l1y, double l2x, double l2y) {
+ double x, y;
+ double dx = l2x - l1x;
+ double dy = l2y - l1y;
+ if (dx == 0.0 && dy == 0.0) {
+ x = l1x;
+ y = l1y;
+ } else {
+ double u;
+ u = ((px-l1x)*dx + (py-l1y)*dy) / (dx*dx+dy*dy);
+ if (u <= 0.0) {
+ x = l1x;
+ y = l1y;
+ } else if (u >= 1.0) {
+ x = l2x;
+ y = l2y;
+ } else {
+ x = l1x + u*dx;
+ y = l1y + u*dy;
+ }
+ }
+ return new double []{x, y};
+ }
+
+ // Distance from point (p) to finite line (l1 to l2)
+ public static double getLineDistance(double px, double py, double l1x, double l1y, double l2x, double l2y) {
+ double x, y;
+ double dx = l2x - l1x;
+ double dy = l2y - l1y;
+ if (dx == 0.0 && dy == 0.0) {
+ x = l1x;
+ y = l1y;
+ } else {
+ double u;
+ u = ((px-l1x)*dx + (py-l1y)*dy) / (dx*dx+dy*dy);
+ if (u <= 0.0) {
+ x = l1x;
+ y = l1y;
+ } else if (u >= 1.0) {
+ x = l2x;
+ y = l2y;
+ } else {
+ x = l1x + u*dx;
+ y = l1y + u*dy;
+ }
+ }
+ return Math.hypot(px-x, py-y);
+ }
+
+ // Shorten line with 'by' from (l1x,l1y)
+ public static double[] shortenLine(double l1x, double l1y, double l2x, double l2y, double by) {
+ double dx = l2x - l1x;
+ double dy = l2y - l1y;
+ if ((dx == 0.0 && dy == 0.0) || (by <= 0)) {
+ return new double []{l1x, l1y};
+ }
+ double len = Math.hypot(dx, dy);
+ if (len <= by) {
+ return new double []{l2x, l2y};
+ }
+ double x, y, u;
+ u = by/len;
+ x = l1x + u*dx;
+ y = l1y + u*dy;
+ return new double []{x, y};
+ }
+}
diff --git a/src/dk/network42/osmfocus/MainActivity.java b/src/dk/network42/osmfocus/MainActivity.java
new file mode 100644
index 0000000..8ecef77
--- /dev/null
+++ b/src/dk/network42/osmfocus/MainActivity.java
@@ -0,0 +1,705 @@
+package dk.network42.osmfocus;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.res.Configuration;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.GpsSatellite;
+import android.location.GpsStatus;
+import android.location.Location;
+import android.location.LocationManager;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.view.GestureDetectorCompat;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesClient;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.location.LocationClient;
+import com.google.android.gms.location.LocationListener;
+import com.google.android.gms.location.LocationRequest;
+
+public class MainActivity extends Activity implements
+ GooglePlayServicesClient.ConnectionCallbacks,
+ GooglePlayServicesClient.OnConnectionFailedListener,
+ LocationListener, GpsStatus.Listener,
+ SensorEventListener {
+
+ static final int CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000;
+ static final int PREFERENCE_REQUEST = 9001;
+ static final int INVALIDATE_VIEW = 1000;
+ static final int POLL_NOTIFICATIONS = 1001;
+
+ private static final String TAG = "OsmFocusActivity";
+ public static final String PREFS_NAME = "OSMFocusPrefsFile";
+
+ LocationManager mLocationManager;
+ LocationClient mLocationClient;
+ LocationRequest mLocationRequest;
+
+ SensorManager sensorManager;
+ private Sensor sensorAccelerometer;
+ private Sensor sensorMagneticField;
+ private float[] valuesAccelerometer;
+ private float[] valuesMagneticField;
+ private float[] matrixR;
+ private float[] matrixI;
+ private float[] matrixValues;
+ private MapView mapView;
+ double mPanLon, mPanLat;
+ private OsmServer mOsmServer = new OsmServer(null, "Editor");
+ private GestureDetectorCompat mGestureDetector;
+ private ScaleGestureDetector mScaleGestureDetector;
+ private boolean mScaleInProgress = false;
+ SharedData mG = null;
+ public double mLonLastUpd=0, mLatLastUpd=0; // Where mapview was last updated
+
+ NotificationManager mNotificationManager;
+ Handler mHandler;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ SharedData.checkAppUpdate(getApplicationContext());
+ PreferenceManager.setDefaultValues(this, R.xml.preferences, true);
+
+ //setContentView(R.layout.activity_main);
+ //getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ // WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ //requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getActionBar().setDisplayShowTitleEnabled(false);
+
+ Log.i(TAG, "API level "+Build.VERSION.SDK_INT);
+
+ mHandler = new Handler(Looper.getMainLooper()) {
+ int mNotifId;
+ boolean mNotifActive;
+ @Override
+ public void handleMessage(Message inputMessage) {
+ switch (inputMessage.what) {
+ case INVALIDATE_VIEW:
+ //Log.d(TAG, "Invalidate view");
+ mapView.invalidate();
+ // Fall-through
+ case POLL_NOTIFICATIONS:
+ int dlsb = mG.mTileLayerProvider.getActiveDownloads();
+ int dlsv = mG.mVectorLayerProvider.getActiveDownloads();
+ Log.d(TAG, "Downloads active="+dlsb+"+"+dlsv);
+ if (dlsb>0 || dlsv>0) {
+ if (! mNotifActive) {
+ String st = "Downloading...";
+ mNotifId = setOsmLoadNotif(st);
+ mNotifActive = true;
+ } else {
+ Log.d(TAG, "Notification already active");
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(POLL_NOTIFICATIONS, this), 1000);
+ }
+ } else if (mNotifActive) {
+ cancelOsmLoadNotif(mNotifId);
+ mNotifActive = false;
+ }
+ break;
+ }
+ }
+ };
+
+ mapView = new MapView(this);
+ //mG.mPcfg.update(getBaseContext());
+ setContentView(mapView);
+ mapView.requestFocus();
+ if (getLastNonConfigurationInstance() != null) {
+ Log.d(TAG, "getLastNonConfigurationInstance() != null");
+ mG = (SharedData) getLastNonConfigurationInstance();
+ mapView.setSharedData(mG);
+ mG.mTileLayer.setMainHandler(mHandler);
+ mG.mVectorLayer.setMainHandler(mHandler);
+ mG.update(getApplicationContext());
+ } else {
+ long maxMemL = Runtime.getRuntime().maxMemory();
+ int maxMem = (int) Math.min(maxMemL, Integer.MAX_VALUE);
+ Log.i(TAG, "maxMemory="+maxMem);
+ mG = new SharedData();
+ mapView.setSharedData(mG);
+ mG.mTileLayerProvider = new OsmTileProvider(mG.mOsmServerAgentName, OsmTile.MAX_DOWNLOAD_THREADS);
+ mG.mTileLayer = new OsmTileLayerBm(mG.mTileLayerProvider, maxMem/4);
+ mG.mTileLayer.setAttrib(getApplicationContext().getString(R.string.info_osm_copyright));
+ mG.mTileLayer.setMainHandler(mHandler);
+ mG.mTileLayer.setProviderUrl(OsmTileLayer.urlFromType(mG.mPcfg.mBackMapType));
+ mG.mVectorLayerProvider = new OsmTileProvider(mG.mOsmServerAgentName);
+ mG.mVectorLayer = new OsmTileLayerVector(mG.mVectorLayerProvider, maxMem/8);
+ mG.mVectorLayer.setSharedData(mG);
+ mG.mVectorLayer.setMainHandler(mHandler);
+ mG.update(getApplicationContext());
+ }
+
+ if (mG.mDeveloperMode) {
+ if(!(Thread.getDefaultUncaughtExceptionHandler() instanceof CustomExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new CustomExceptionHandler(mG, ""));
+ }
+ }
+
+ mGestureDetector = new GestureDetectorCompat(this, mGestureListener);
+ mScaleGestureDetector = new ScaleGestureDetector(this, mScaleGestureListener);
+ /*mDetector.setOnDoubleTapListener(this);*/
+
+ mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+
+ mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+ mLocationManager.addGpsStatusListener(this);
+ Location loc = getMostRecentKnownLocation();
+ if (loc != null)
+ this.onLocationChanged(loc);
+
+ mLocationClient = new LocationClient(this, this, this);
+ mLocationRequest = LocationRequest.create();
+ mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
+ mLocationRequest.setInterval(1000/*ms*/);
+ mLocationRequest.setFastestInterval(500/*ms*/);
+
+ //locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+ //if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
+ // Intent intent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ // startActivity(intent);
+ //}
+ //Criteria criteria = new Criteria();
+ //mG.mLocProvider = locationManager.getBestProvider(criteria, false);
+ //mG.mLocProvider = locationManager.GPS_PROVIDER;
+ //Location location = locationManager.getLastKnownLocation(mG.mLocProvider);
+ //if (location != null) {
+ // Toast toast = Toast.makeText(getApplicationContext(), "Location provider "+mG.mLocProvider, Toast.LENGTH_SHORT);
+ // toast.show();
+ // //onLocationChanged(location);
+ //}
+
+ //Location location = new Location("FIXME");
+ //Location location = mLocationClient.getLastLocation();
+
+ if (mG.mUseCompass) {
+ sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
+ sensorAccelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ sensorMagneticField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+ valuesAccelerometer = new float[3];
+ valuesMagneticField = new float[3];
+ matrixR = new float[9];
+ matrixI = new float[9];
+ matrixValues = new float[3];
+ }
+
+ // Initialize the location fields
+ //if (location != null) {
+ // Log.d(TAG, "Provider " + mG.mLocProvider + " has been selected.");
+ // onLocationChanged(location);
+ //} else {
+ //latituteField.setText("Location not available");
+ //longitudeField.setText("Location not available");
+ //}
+
+ //registerForContextMenu(mapView);
+
+ registerComponentCallbacks(new ComponentCallbacks2() {
+ @ Override
+ public void onTrimMemory(int level) {
+ appTrimMemory(level);
+ }
+ public void onLowMemory() {
+ appTrimMemory(TRIM_MEMORY_COMPLETE);
+ }
+ public void onConfigurationChanged(Configuration newConfig) {
+ //
+ }
+ });
+ }
+
+ public void appTrimMemory(int level) {
+ Log.d(TAG, "appTrimMemory("+level+")");
+ if (mG.mTileLayer != null)
+ mG.mTileLayer.onTrimMemory(level);
+ if (mG.mVectorLayer != null)
+ mG.mVectorLayer.onTrimMemory(level);
+ }
+
+ public Object onRetainNonConfigurationInstance() {
+ if (mG != null)
+ return mG;
+ return super.onRetainNonConfigurationInstance();
+ }
+
+ /*
+ * Called when the Activity becomes visible.
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mLocationClient.connect();
+ }
+
+ /*
+ * Called when the Activity is no longer visible.
+ */
+ @Override
+ protected void onStop() {
+ if (mLocationClient.isConnected()) {
+ mLocationClient.removeLocationUpdates(this);
+ }
+ mLocationClient.disconnect();
+ super.onStop();
+ }
+
+ // Define a DialogFragment that displays the error dialog
+ public static class ErrorDialogFragment extends DialogFragment {
+ // Global field to contain the error dialog
+ private Dialog mDialog;
+ // Default constructor. Sets the dialog field to null
+ public ErrorDialogFragment() {
+ super();
+ mDialog = null;
+ }
+ // Set the dialog to display
+ public void setDialog(Dialog dialog) {
+ mDialog = dialog;
+ }
+ // Return a Dialog to the DialogFragment.
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return mDialog;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case CONNECTION_FAILURE_RESOLUTION_REQUEST:
+ /*
+ * If the result code is Activity.RESULT_OK, try
+ * to connect again
+ */
+ switch (resultCode) {
+ case Activity.RESULT_OK:
+ break;
+ }
+ case PREFERENCE_REQUEST:
+ mG.update(getBaseContext());
+ mapView.postInvalidate();
+ break;
+ }
+ }
+
+ private boolean servicesConnected() {
+ int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
+ if (ConnectionResult.SUCCESS == resultCode) {
+ Log.d("Location Updates", "Google Play services is available.");
+ return true;
+ } else {
+ //int errorCode = GooglePlayServicesUtil.getErrorCode();
+ Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(resultCode, this,
+ CONNECTION_FAILURE_RESOLUTION_REQUEST);
+ if (errorDialog != null) {
+ errorDialog.show();
+ } else {
+ showOkDialog(this, "Something went wrong with Google Play Services");
+ }
+ return false;
+ }
+ }
+
+ public static void showOkDialog(Context context, String txt)
+ {
+ Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(txt);
+ builder.setCancelable(true);
+ builder.setPositiveButton("OK", null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ public void onConnected(Bundle dataBundle) {
+ Log.d(TAG, "onConnected");
+ mLocationClient.requestLocationUpdates(mLocationRequest, this);
+ }
+
+ @Override
+ public void onDisconnected() {
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult connectionResult) {
+ if (connectionResult.hasResolution()) {
+ try {
+ // Start an Activity that tries to resolve the error
+ connectionResult.startResolutionForResult(
+ this,
+ CONNECTION_FAILURE_RESOLUTION_REQUEST);
+ /*
+ * Thrown if Google Play services canceled the original
+ * PendingIntent
+ */
+ } catch (IntentSender.SendIntentException e) {
+ // Log the error
+ e.printStackTrace();
+ }
+ } else {
+ /*
+ * If no resolution is available, display a dialog to the
+ * user with the error.
+ */
+ //showErrorDialog(connectionResult.getErrorCode());
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event){
+ boolean h = mScaleGestureDetector.onTouchEvent(event);
+ h |= mGestureDetector.onTouchEvent(event);
+ h |= super.onTouchEvent(event);
+ return h;
+ }
+
+ private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
+ = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
+ float focusX = scaleGestureDetector.getFocusX();
+ float focusY = scaleGestureDetector.getFocusY();
+ Log.d(TAG, "ScaleBegin, focus="+focusX+","+focusY);
+ return true;
+ }
+ @Override
+ public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
+ float focusX = scaleGestureDetector.getFocusX();
+ float focusY = scaleGestureDetector.getFocusY();
+ float scale = scaleGestureDetector.getScaleFactor();
+ Log.d(TAG, "Scale, scale="+scale+", focus="+focusX+","+focusY);
+ mG.mPcfg.setScale(mG.mPcfg.getScale()*scale, mG.mCtx);
+ mScaleInProgress = true;
+ mapView.postInvalidate();
+ return true;
+ }
+ @Override
+ public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
+ Log.d(TAG, "ScaleEnd");
+ }
+ };
+
+ private final GestureDetector.SimpleOnGestureListener mGestureListener
+ = new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDown(MotionEvent event) {
+ //Log.d(TAG,"onDown: " + event.toString());
+ mPanLon = mG.mLon; mPanLat = mG.mLat;
+ mScaleInProgress = false;
+ return true;
+ }
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ float distanceY) {
+ if (mScaleInProgress) {
+ Log.d(TAG, "Scroll skipped");
+ return false;
+ }
+ //Log.d(TAG, "onScroll: " + e1.toString()+e2.toString());
+ float dx, dy;
+ dx = e2.getX()-e1.getX();
+ dy = e2.getY()-e1.getY();
+ Log.d(TAG, "Scroll pixels: x="+dx+" y="+dy);
+ mG.mLon = mPanLon - dx/(mG.mPcfg.mScale*MapView.prescale);
+ double panMercLat = dy/(mG.mPcfg.mScale*MapView.prescale);
+ mG.mLat = GeoMath.mercatorToLat((GeoMath.latToMercator(mPanLat)+panMercLat));
+ Log.d(TAG, "Scroll to lon="+mG.mLon+" lat="+mG.mLat+" ("+GeoMath.latToMercator(mPanLat)+"+"+panMercLat+")");
+ mG.mFollowGPS = false;
+ mapView.postInvalidate();
+ return true;
+ }
+ @Override
+ public boolean onFling(MotionEvent event1, MotionEvent event2,
+ float velocityX, float velocityY) {
+ Log.d(TAG, "onFling: " + event1.toString()+event2.toString());
+ return true;
+ }
+
+// @Override
+// public void onLongPress(MotionEvent event) {
+// Log.d(TAG, "onLongPress: " + event.toString());
+// openOptionsMenu();
+// }
+ //@Override
+ //public void onShowPress(MotionEvent event) {
+ // Log.d(TAG, "onShowPress: " + event.toString());
+ //}
+
+ //@Override
+ //public boolean onSingleTapUp(MotionEvent event) {
+ // Log.d(TAG, "onSingleTapUp: " + event.toString());
+ // return true;
+ // }
+ };
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main_activity_actions, menu);
+ if (mG.mDeveloperMode) {
+ menu.add(0, R.id.action_testdownload, Menu.NONE, R.string.action_testdownload);
+ menu.add(0, R.id.action_togglehud, Menu.NONE, R.string.action_togglehud);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ //@Override
+ //public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ // super.onCreateContextMenu(menu, v, menuInfo);
+ // getMenuInflater().inflate(R.menu.main, menu);
+ //}
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == R.id.action_download) {
+ if (mG.mPhyLocation != null) {
+ Log.d(TAG, "Download");
+ //float horizon = 250f; //meters
+ //GeoBBox bbox = OsmServer.getBoxForPoint(mG.mLat, mG.mLon, horizon);
+ //GeoBBox bbox = OsmTile.pos2BBox(mG.mLon, mG.mLat, 16);
+ //downloadBox(bbox);
+ // FIXME
+ mG.mVectorLayer.download(mG.mLon, mG.mLat);
+ } else {
+ Toast toast = Toast.makeText(getApplicationContext(), getBaseContext().getString(R.string.info_locationunknown), Toast.LENGTH_SHORT);
+ toast.show();
+ }
+ return true;
+// } else if (itemId == R.id.action_testdownload) {
+// Log.d(TAG, "Test Download");
+// float horizon = 250f; //meters
+// GeoBBox bbox = OsmServer.getBoxForPoint(mG.mLat, mG.mLon, horizon);
+// olddownloadBox(bbox);
+// return true;
+// } else if (itemId == R.id.action_loadcache) {
+// Log.d(TAG, "Load Cache");
+// readOSMFile("cache.osm");
+// return true;
+ } else if (itemId == R.id.action_whereami) {
+ Log.d(TAG, "Where am I?");
+ mG.mFollowGPS = true;
+ mG.mPcfg.setScale(PaintConfig.DEFAULT_ZOOM, mG.mCtx);
+ if (mG.mPhyLocation != null) {
+ mG.mLat = mG.mPhyLocation.getLatitude();
+ mG.mLon = mG.mPhyLocation.getLongitude();
+ mapView.postInvalidate();
+ } else {
+ Toast toast = Toast.makeText(getApplicationContext(), getBaseContext().getString(R.string.info_locationunknown), Toast.LENGTH_SHORT);
+ toast.show();
+ }
+ return true;
+ } else if (itemId == R.id.action_togglehud) {
+ mG.mDebugHUD = !mG.mDebugHUD;
+ mapView.invalidate();
+ return true;
+ } else if (itemId == R.id.action_settings) {
+ startActivityForResult(new Intent(this, SettingsActivity.class), PREFERENCE_REQUEST);
+ mG.update(getApplicationContext());
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ //locationManager.requestLocationUpdates(mG.mLocProvider, 1000/*ms*/, 1/*meters*/, this);
+ if (mG.mUseCompass) {
+ sensorManager.registerListener(this,
+ sensorAccelerometer,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ sensorManager.registerListener(this,
+ sensorMagneticField,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mG.mUseCompass) {
+ sensorManager.unregisterListener(this,
+ sensorAccelerometer);
+ sensorManager.unregisterListener(this,
+ sensorMagneticField);
+ }
+ }
+
+ protected Location getMostRecentKnownLocation() {
+ Location loc = null;
+ long besttime = 0;
+ List allp = mLocationManager.getAllProviders();
+ for (String p: allp) {
+ Location l = mLocationManager.getLastKnownLocation(p);
+ if (l != null) {
+ long time = l.getTime();
+ if (time>besttime) {
+ loc = l;
+ besttime = time;
+ }
+ }
+ }
+ return loc;
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ mG.mPhyLocation = location;
+ mG.mLocationUpdates++;
+ double curr_lon = mG.mPhyLocation.getLongitude();
+ double curr_lat = mG.mPhyLocation.getLatitude();
+ if (mG.mFollowGPS) {
+ mG.mLon = curr_lon;
+ mG.mLat = curr_lat;
+ }
+ //Log.d(TAG, "onLocationChanged, Loc="+mG.mPhyLocation);
+ final double noise = 0.00002;
+ if (Math.abs(mLonLastUpd-curr_lon)>noise || Math.abs(mLatLastUpd-curr_lat)>noise) {
+ mLonLastUpd = curr_lon;
+ mLatLastUpd = curr_lat;
+ mapView.postInvalidate();
+ }
+ }
+
+ public void onGpsStatusChanged(int event) {
+ boolean statchg = false;
+ switch (event) {
+ case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
+ GpsStatus gstat = mLocationManager.getGpsStatus(null);
+ if (gstat != null) {
+ Iterable satellites = gstat.getSatellites();
+ Iterator sat = satellites.iterator();
+ int num = 0, used = 0;
+ while (sat.hasNext()) {
+ num++;
+ GpsSatellite satellite = sat.next();
+ if (satellite.usedInFix()) {
+ used++;
+ }
+ }
+ if (mG.mSatelitesVisible != num || mG.mSatelitesUsed != used)
+ statchg = true;
+ mG.mSatelitesVisible = num;
+ mG.mSatelitesUsed = used;
+ }
+ break;
+ }
+ if (statchg)
+ mapView.postInvalidate();
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+
+ switch(event.sensor.getType()){
+ case Sensor.TYPE_ACCELEROMETER:
+ for(int i =0; i < 3; i++){
+ valuesAccelerometer[i] = event.values[i];
+ }
+ break;
+ case Sensor.TYPE_MAGNETIC_FIELD:
+ for(int i =0; i < 3; i++){
+ valuesMagneticField[i] = event.values[i];
+ }
+ break;
+ }
+
+ boolean success = SensorManager.getRotationMatrix(
+ matrixR,
+ matrixI,
+ valuesAccelerometer,
+ valuesMagneticField);
+
+ if(success){
+ SensorManager.getOrientation(matrixR, matrixValues);
+
+ mG.mAzimuth = Math.toDegrees(matrixValues[0]);
+ mG.mPitch = Math.toDegrees(matrixValues[1]);
+ mG.mRoll = Math.toDegrees(matrixValues[2]);
+ //Log.d(TAG, "Orientation: Azimuth " + azimuth + ", Pitch " + pitch + ", Roll " + roll);
+ mapView.postInvalidate();
+
+ }
+ }
+
+ int setOsmLoadNotif(String info) {
+ int notifyID = 1;
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(/*this*/getApplicationContext())
+ .setContentTitle("Downloading OSM Data")
+ .setContentText(info)
+ .setSmallIcon(R.drawable.ic_downloadosm);
+// Intent resultIntent = new Intent(/*this*/getApplicationContext(), MainActivity.class); //FIXME
+// PendingIntent resultPendingIntent =
+// PendingIntent.getActivity(
+// /*this*/getApplicationContext(),
+// 0,
+// resultIntent,
+// PendingIntent.FLAG_UPDATE_CURRENT
+// );
+// mBuilder.setContentIntent(resultPendingIntent);
+ mNotificationManager.notify(
+ notifyID,
+ mBuilder.build());
+ Log.d(TAG, "Set OSM notif, id="+notifyID);
+ return notifyID;
+ }
+
+ void cancelOsmLoadNotif(int notifyID) {
+ Log.d(TAG, "Clear OSM notif, id="+notifyID);
+ mNotificationManager.cancel(notifyID);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+}
diff --git a/src/dk/network42/osmfocus/MapLayer.java b/src/dk/network42/osmfocus/MapLayer.java
new file mode 100644
index 0000000..4358820
--- /dev/null
+++ b/src/dk/network42/osmfocus/MapLayer.java
@@ -0,0 +1,14 @@
+package dk.network42.osmfocus;
+
+import android.graphics.Canvas;
+import android.graphics.RectF;
+
+abstract public class MapLayer {
+ static final int MAPTYPE_NONE = 1;
+ static final int MAPTYPE_OSM = 2;
+ static final int MAPTYPE_OSMCYCLEMAP = 3;
+ static final int MAPTYPE_INTERNAL = 4;
+
+ abstract public void draw(Canvas canvas, RectF worldport, PaintConfig pcfg);
+ public void onTrimMemory(int level) {}
+}
diff --git a/src/dk/network42/osmfocus/MapView.java b/src/dk/network42/osmfocus/MapView.java
new file mode 100644
index 0000000..67519d5
--- /dev/null
+++ b/src/dk/network42/osmfocus/MapView.java
@@ -0,0 +1,165 @@
+package dk.network42.osmfocus;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.location.GpsStatus;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
+
+public class MapView extends View {
+ private static final String TAG = "MapView";
+ String mLocProvider, mTmpStr;
+ GpsStatus mGpsStatus;
+ long mLastRedraw;
+ SharedData mG;
+ ArrayList mNearest = new ArrayList(10);
+ Rect viewrect = new Rect();
+ RectF viewrectf = new RectF();
+ RectF worldrect = new RectF();
+ int mRedraws=0;
+ Context context;
+ static public final float prescale = 150000.0f;
+
+ public MapView(Context ctx) {
+ super(ctx);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ context = ctx;
+ mLastRedraw = SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ long now = SystemClock.elapsedRealtime();
+
+ canvas.drawARGB(255, 238, 241, 233);
+
+ canvas.getClipBounds(viewrect);
+ int h = viewrect.height();
+ int w = viewrect.width();
+ viewrectf.set(viewrect);
+ //Log.d(TAG, "Viewrectf: "+viewrectf.toString());
+ mG.mPcfg.viewmatrix.reset();
+ canvas.save();
+ //canvas.getMatrix().mapRect(viewrectf);
+ //Log.d(TAG, "2 Viewrectf: "+viewrectf.toString());
+ canvas.translate(w/2, h/2);
+ mG.mPcfg.viewmatrix.preTranslate(w/2, h/2);
+ if (mG.mUseCompass) {
+ canvas.rotate((float)-mG.mAzimuth);
+ }
+
+ canvas.save();
+
+ canvas.scale(1.0f, -1.0f); // Up is down
+ mG.mPcfg.viewmatrix.preScale(1.0f, -1.0f);
+
+ canvas.scale(mG.mPcfg.mScale, mG.mPcfg.mScale); // Current zoom
+ mG.mPcfg.viewmatrix.preScale(mG.mPcfg.mScale, mG.mPcfg.mScale);
+
+ canvas.translate((float)(-mG.mLon*MapView.prescale), (float)(-GeoMath.latToMercator(mG.mLat)*MapView.prescale)); // Current pos
+ mG.mPcfg.viewmatrix.preTranslate((float)(-mG.mLon*MapView.prescale), (float)(-GeoMath.latToMercator(mG.mLat)*MapView.prescale)); // Current pos
+ worldrect.set(viewrectf);
+ mG.mPcfg.view2world(worldrect);
+ //Log.d(TAG, "Viewport="+viewrectf+", world viewport="+worldrect);
+
+ if (mG.mLocationUpdates > 0) {
+ if (mG.mTileLayer != null && (mG.mPcfg.mBackMapType == MapLayer.MAPTYPE_OSM || mG.mPcfg.mBackMapType == MapLayer.MAPTYPE_OSMCYCLEMAP)) {
+ mG.mTileLayer.draw(canvas, worldrect, mG.mPcfg);
+ }
+ if (mG.mVectorLayer != null) {
+ mG.mVectorLayer.draw(canvas, worldrect, mG.mPcfg);
+ }
+ }
+
+ // GPS position
+ if (mG.mPhyLocation != null) {
+ canvas.drawCircle((float)(mG.mPhyLocation.getLongitude()*MapView.prescale),
+ (float)(GeoMath.latToMercator(mG.mPhyLocation.getLatitude())*MapView.prescale),
+ (float)(GeoMath.convertMetersToGeoDistanceMed((float)mG.mPhyLocation.getAccuracy())*MapView.prescale), mG.mPcfg.gps);
+ }
+
+ canvas.restore(); // Restores zoom scaling, current position translation and 'up is down'
+
+ if (mG.mPhyLocation == null) {
+ String txt = context.getString(R.string.info_locationunknown);
+ canvas.drawText(txt, -mG.mPcfg.debughud.measureText(txt)/2, -3*mG.mPcfg.tagtextsize, mG.mPcfg.debughud);
+ txt = "Satelites: Visible:"+mG.mSatelitesVisible+" used:"+mG.mSatelitesUsed;
+ canvas.drawText(txt, -mG.mPcfg.debughud.measureText(txt)/2, -2*mG.mPcfg.tagtextsize, mG.mPcfg.debughud);
+ } else {
+ // Compass
+// canvas.save();
+// canvas.translate(w/2, h/2);
+// canvas.scale(w/2, h/2); //FIXME
+// //canvas.drawLine(0, 0, FloatMath.cos(aarad), FloatMath.sin(aarad), mG.mPcfg.paint);
+// //canvas.drawCircle(FloatMath.cos(aarad), FloatMath.sin(aarad), 5, mG.mPcfg.paint);
+// //canvas.drawCircle(0, 0, 1, mG.mPcfg.paint);
+
+ // Cross-hair
+ float len1 = 2.0f*mG.mPcfg.densityScale;
+ float len2 = 10.0f*mG.mPcfg.densityScale;
+ canvas.drawLine(0, -len1, 0, -len2, mG.mPcfg.gps2);
+ canvas.drawLine(0, len1, 0, len2, mG.mPcfg.gps2);
+ canvas.drawLine(-len1, 0, -len2, 0, mG.mPcfg.gps2);
+ canvas.drawLine( len1, 0, len2, 0, mG.mPcfg.gps2);
+ }
+
+ canvas.restore(); // Restores center (0,0)
+
+ // Tags
+ if (mG.mLocationUpdates > 0) {
+ if (mG.mTileLayer != null) {
+ mG.mTileLayer.drawLegends(canvas, viewrectf, mG.mPcfg);
+ }
+ if (mG.mVectorLayer != null) {
+ mG.mVectorLayer.drawLegends(canvas, viewrectf, mG.mPcfg);
+ }
+ }
+
+ canvas.restore();
+ long endtime = SystemClock.elapsedRealtime();
+
+ // Debug info
+ float dbghudY = h/2;
+ if (mG.mDebugHUD) {
+ canvas.drawText(String.format("View Loc: %.8f,%.8f, upd %d, sats %d/%d, scale %.4f",
+ mG.mLon, mG.mLat, mG.mLocationUpdates, mG.mSatelitesVisible, mG.mSatelitesUsed, mG.mPcfg.mScale),
+ 0, dbghudY+mG.mPcfg.mHUDspacing, mG.mPcfg.debughud);
+ if (mG.mPhyLocation != null) {
+ canvas.drawText(String.format("Phy Loc: %.8f,%.8f, Accuracy: %.4f (%s)",
+ mG.mPhyLocation.getLongitude(), mG.mPhyLocation.getLatitude(), mG.mPhyLocation.getAccuracy(), mLocProvider),
+ 0, dbghudY+2*mG.mPcfg.mHUDspacing, mG.mPcfg.debughud);
+ } else {
+ canvas.drawText("No phy location yet",
+ 0, dbghudY+2*mG.mPcfg.mHUDspacing, mG.mPcfg.debughud);
+ }
+ canvas.drawText(String.format("Azimuth: %.4f, Pitch: %.4f, Roll: %.4f", mG.mAzimuth, mG.mPitch, mG.mRoll),
+ 0, dbghudY+3*mG.mPcfg.mHUDspacing, mG.mPcfg.debughud);
+ if (mG.mDb != null) {
+ canvas.drawText(String.format("Nodes: %d, Ways: %d, Relations: %d",
+ mG.mDb.getNodes().size(), mG.mDb.getWays().size(), mG.mDb.getRelations().size()),
+ 0, dbghudY+4*mG.mPcfg.mHUDspacing, mG.mPcfg.debughud);
+ }
+ canvas.drawText(String.format("RedrawPeriod: %dms, RedrawTime: %dms, Redraws:%d",
+ now-mLastRedraw, endtime-now, ++mRedraws), 0, dbghudY+5*mG.mPcfg.mHUDspacing, mG.mPcfg.debughud);
+ int memTile=0, memXml=0;
+ if (mG.mTileLayer!=null)
+ memTile = mG.mTileLayer.getMemorySize();
+ if (mG.mVectorLayer!=null)
+ memXml = mG.mVectorLayer.getMemorySize();
+ canvas.drawText(String.format("MemoryUse: Tiles=%dk, Vector: %dk",
+ memTile/1024, memXml/1024), 0, dbghudY+6*mG.mPcfg.mHUDspacing, mG.mPcfg.debughud);
+ }
+ //Log.d(TAG, "h="+h+"spacing="+mG.mPcfg.mListSpacing);
+ mLastRedraw = now;
+ }
+
+ public void setSharedData(SharedData data) {
+ mG = data;
+ }
+}
diff --git a/src/dk/network42/osmfocus/OsmBounds.java b/src/dk/network42/osmfocus/OsmBounds.java
new file mode 100644
index 0000000..75cc79a
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmBounds.java
@@ -0,0 +1,45 @@
+package dk.network42.osmfocus;
+
+import android.graphics.Canvas;
+
+public class OsmBounds {
+ private static final String TAG = "OsmBounds";
+
+ public
+ OsmNode min = null, max = null;
+
+ public boolean isEmpty() {
+ return (min==null || max==null);
+ }
+
+ public OsmBounds() {
+ }
+
+ public OsmBounds(int minlon, int minlat, int maxlon, int maxlat) {
+ // Normalize by swapping
+ int tmp;
+ if (minlon>maxlon) {
+ tmp = minlon;
+ minlon = maxlon;
+ maxlon = tmp;
+ }
+ if (minlat>maxlat) {
+ tmp = minlat;
+ minlat = maxlat;
+ maxlat = tmp;
+ }
+
+ min = new OsmNode(minlon, minlat);
+ max = new OsmNode(maxlon, maxlat);
+ }
+
+ // Osm geopoints, lon/lat * 1e7
+ int getLeft() { return min.getOsmLon(); }
+ int getTop() { return min.getOsmLat(); }
+ int getRight() { return max.getOsmLon(); }
+ int getBottom() { return max.getOsmLat(); }
+
+ public void draw(Canvas canvas, OsmDB db, PaintConfig pcfg) {
+ canvas.drawRect(min.getX(db), min.getY(db), max.getX(db), max.getY(db), pcfg.bounds);
+ }
+}
diff --git a/src/dk/network42/osmfocus/OsmDB.java b/src/dk/network42/osmfocus/OsmDB.java
new file mode 100644
index 0000000..ec4cc6d
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmDB.java
@@ -0,0 +1,396 @@
+package dk.network42.osmfocus;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.util.Log;
+
+public class OsmDB extends MapLayer {
+ private static final String TAG = "OsmDB";
+ private long mWayId = 0;
+ private long mNodeId = 0;
+ private Map mNodes;
+ private Map mWays;
+ private Map mRels;
+ private ArrayList mBounds;
+ private Filter mFilter = new Filter();
+
+ float mX, mY, mYmerc; // Current view position, latitude is Mercator corrected
+
+ OsmDB() {
+ mNodes = new HashMap();
+ mWays = new HashMap();
+ mRels = new HashMap();
+ mBounds = new ArrayList();
+ }
+
+ public OsmBounds getBounds() { // FIXME: Not really correct with multiple bounds
+ return mBounds.get(0);
+ }
+
+ //void setBBox(final GeoBBox bbox) {
+ // this.box = bbox;
+ //}
+
+ // True if we have any bounds defined. May contain zero nodes/ways
+ public boolean isEmpty() {
+ return (mBounds.isEmpty() || mBounds.get(0).isEmpty());
+ }
+
+ public OsmNode getNode(final long osmId) {
+ return mNodes.get(osmId);
+ }
+
+ public OsmWay getWay(final long osmId) {
+ return mWays.get(osmId);
+ }
+
+ public OsmWay getRelation(final long osmId) {
+ return mWays.get(osmId);
+ }
+
+ public OsmElement getElement(final long osmId) {
+ OsmElement elem = mWays.get(osmId);
+ if (elem != null)
+ return elem;
+ elem = mNodes.get(osmId);
+ if (elem != null)
+ return elem;
+ elem = mRels.get(osmId);
+ if (elem != null)
+ return elem;
+ return null;
+ }
+
+ public Map getNodes() {
+ return mNodes;
+ }
+
+ public Map getWays() {
+ return mWays;
+ }
+
+ public Map getRelations() {
+ return mRels;
+ }
+
+ void insertNode(final OsmNode node) {
+ mNodes.put(node.mOsmId, node);
+ node.mDb = this;
+ }
+
+ void insertWay(final OsmWay way) {
+ mWays.put(way.mOsmId, way);
+ way.mDb = this;
+ }
+
+ void insertRelation(final OsmRelation rel) {
+ mRels.put(rel.mOsmId, rel);
+ rel.mDb = this;
+ }
+
+ void insertBounds(final OsmBounds box) {
+ mBounds.add(box);
+ if (mBounds.size()==1) {
+ mX = (float) (getBounds().getLeft()/1e7f);
+ mY = (float) (getBounds().getTop()/1e7f);
+ mYmerc = (float) GeoMath.latToMercator(mY);
+ //Log.d(TAG, "DB ref frozen to "+mX+", "+mY+"("+mYmerc+")");
+ }
+ }
+
+ public static OsmNode createNode(long osmId, long osmVersion, int lat, int lon) {
+ return new OsmNode(osmId, osmVersion, lat, lon);
+ }
+
+ public OsmNode createNodeWithNewId(int lat, int lon) {
+ return createNode(--mNodeId, 1, lat, lon);
+ }
+
+ public static OsmWay createWay(long osmId, long osmVersion) {
+ return new OsmWay(osmId, osmVersion);
+ }
+
+ public OsmWay createWayWithNewId() {
+ return createWay(--mWayId, 1);
+ }
+
+ public static OsmRelation createRelation(long osmId, long osmVersion) {
+ return new OsmRelation(osmId, osmVersion);
+ }
+
+ public OsmRelation createRelationWithNewId() {
+ return createRelation(--mWayId, 1);
+ }
+
+ public static OsmBounds createBounds(int minlon, int minlat, int maxlon, int maxlat) throws OsmException {
+ return new OsmBounds(minlon, minlat, maxlon, maxlat);
+ }
+
+ // FIXME handle overlaps, merge newest
+ void merge(OsmDB with) {
+ if (with==null)
+ return;
+ if (mNodes.size()==0)
+ mNodes = with.mNodes;
+ if (mWays.size()==0)
+ mWays = with.mWays;
+
+ with.mX = mX; // Update with relative origin of dest db
+ with.mY = mY;
+ with.mYmerc = mYmerc;
+ with.updateVisuals();
+
+ Iterator it;
+ it = with.mNodes.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = (Map.Entry) it.next();
+ if (!mNodes.containsKey(pair.getKey())) {
+ insertNode((OsmNode)pair.getValue());
+ }
+ }
+ it = with.mWays.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = (Map.Entry) it.next();
+ if (!mWays.containsKey(pair.getKey())) {
+ insertWay((OsmWay)pair.getValue());
+ }
+ }
+ }
+
+ void updateAndVacuum() {
+ Iterator it = mWays.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = (Map.Entry) it.next();
+ OsmWay ee = (OsmWay)pair.getValue();
+ mFilter.filter(ee);
+ Iterator nit = ee.getNodes().iterator();
+ while (nit.hasNext()) {
+ OsmNode nn = (OsmNode) nit.next();
+ nn.mIsWayNode = true;
+ }
+ }
+ it = mNodes.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = (Map.Entry) it.next();
+ mFilter.filter((OsmElement)pair.getValue());
+ }
+ updateAndVacuumRelations();
+ vacuumWays();
+ updateVisuals();
+ }
+
+ // Remove relation that we do not use
+ void updateAndVacuumRelations() {
+ Iterator it = mRels.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = (Map.Entry) it.next();
+ OsmRelation rel = (OsmRelation) pair.getValue();
+ if (rel.hasTag("type", "multipolygon")) {
+ rel.updateVisualDefiningElem();
+ } else {
+ //Log.d(TAG, "Vacuum Rel, id="+rel.mOsmId);
+ it.remove();
+ }
+ }
+ }
+
+ // Remove e.g. ways tagged with building=yes and which are part of a multipolygon with role=outer
+ void vacuumWays() {
+ Iterator it = mRels.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = (Map.Entry) it.next();
+ OsmRelation rel = (OsmRelation) pair.getValue();
+ if (rel.hasTag("type", "multipolygon") && rel.hasRole("outer")) {
+ long ref = rel.getRoleRef("outer");
+ //Log.d(TAG, "Rel id="+rel.mOsmId+", outer id="+ref);
+ OsmWay way = mWays.get(ref);
+ if (way != null) {
+ way.mIsMultipolyOuter = true;
+ }
+ }
+ }
+ }
+
+ void updateVisuals() {
+ Iterator it = mWays.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = (Map.Entry) it.next();
+ OsmWay way = (OsmWay) pair.getValue();
+ way.updateVisuals(this);
+ }
+ it = mRels.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = (Map.Entry) it.next();
+ OsmRelation rel = (OsmRelation) pair.getValue();
+ rel.updateVisuals(this);
+ }
+ }
+
+ public static boolean idIsInList(long id, ArrayList list) {
+ for (int ii = 0; ii near) {
+ double lat_merc = GeoMath.latToMercator(lat);
+ //Log.d(TAG, "Find elements close to ("+lon+","+lat+"(merc="+lat_merc+"))");
+ OsmElement e;
+ Map.Entry pair;
+ Iterator it = mNodes.entrySet().iterator();
+ while (it.hasNext()) {
+ pair = (Map.Entry) it.next();
+ e = (OsmNode) pair.getValue();
+ if (e.getTagCnt() > 0 && !e.mFiltered && !idIsInList(e.getOsmId(), near)) {
+ e.compareSetDistTo(lon, lat_merc);
+ if (near.size() < max) {
+ near.add(e);
+ //Log.d(TAG, "Node(Id="+e.mOsmId+"): Add, near size="+near.size()+" near=["+dumpNearArray(near)+"]");
+ Collections.sort(near);
+ } else if (near.get(near.size()-1).compareGet() > e.compareGet()) {
+ near.set(near.size()-1, e);
+ Collections.sort(near);
+ }
+ //Log.d(TAG, "Node(Id="+e.mOsmId+"): near size="+near.size()+" near=["+dumpNearArray(near)+"]");
+ }
+ }
+ it = mWays.entrySet().iterator();
+ while (it.hasNext()) {
+ pair = (Map.Entry) it.next();
+ e = (OsmWay) pair.getValue();
+ if (e.getTagCnt() > 0 && !e.mFiltered && !idIsInList(e.getOsmId(), near)) {
+ e.compareSetDistTo(lon, lat_merc);
+ if (near.size() < max) {
+ near.add(e);
+ //Log.d(TAG, "Way(Id="+e.mOsmId+"): Add, near size="+near.size()+" near=["+dumpNearArray(near)+"]");
+ Collections.sort(near);
+ } else if (near.get(near.size()-1).compareGet() > e.compareGet()) {
+ near.set(near.size()-1, e);
+ //Log.d(TAG, "Way(Id="+e.mOsmId+"): insert at "+(near.size()-1)+" near=["+dumpNearArray(near)+"]");
+ Collections.sort(near);
+ }
+ }
+ }
+ //Log.d(TAG, "Final near=["+dumpNearArray(near)+"]");
+ return 1;
+ }
+
+// private static String dumpNearArray(ArrayList near) {
+// String st = "";
+// for (int ii=0; ii near) {
+// String st = "";
+// for (int ii=0; ii near, double lon, double latMerc, double angleOffset) {
+ for (int ii = 0; ii(){
+ @Override
+ public int compare(OsmElement e1, OsmElement e2) {
+ return (int) e2.compareTo(e1);
+ }
+ });
+ //Log.d(TAG, "After sort:"+dumpNearArray(near));
+ }
+
+ // Counter clock-wise
+ public static void radianSortCCW(ArrayList near, double lon, double latMerc, double angleOffset) {
+ for (int ii = 0; ii {
+ private static final String TAG = "OsmElement";
+ protected long mOsmId;
+ protected long mOsmVersion;
+ protected SortedMap tags;
+ protected double mCompare=0; // Cached compare value, only calculate once
+ boolean mFiltered = false;
+ boolean mIsArea = false;
+ OsmDB mDb = null;
+
+ enum Compare implements Comparator {
+ X_COORD {
+ public int compare(OsmElement e1, OsmElement e2) {
+ if (e1.getX() >= e2.getX()) return 1;
+ else return -1;
+ }
+ },
+ X_COORD_DESCENDING {
+ public int compare(OsmElement e1, OsmElement e2) {
+ if (e2.getX() >= e1.getX()) return 1;
+ else return -1;
+ }
+ },
+ Y_COORD {
+ public int compare(OsmElement e1, OsmElement e2) {
+ if (e1.getY() >= e2.getY()) return 1;
+ else return -1;
+ }
+ },
+ Y_COORD_DESCENDING {
+ public int compare(OsmElement e1, OsmElement e2) {
+ if (e2.getY() >= e1.getY()) return 1;
+ else return -1;
+ }
+ },
+ }
+
+ OsmElement(final long id, final long version) {
+ this.mOsmId = id;
+ this.mOsmVersion = version;
+ this.tags = new TreeMap();
+ }
+
+ void addOrUpdateTag(final String tag, final String value) {
+ tags.put(tag, value);
+ }
+ public int getTagCnt() {
+ return tags.size();
+ }
+ public boolean hasTag(final String key, final String value) {
+ String keyValue = tags.get(key);
+ return keyValue != null && keyValue.equals(value);
+ }
+
+ public String getTagWithKey(final String key) {
+ return tags.get(key);
+ }
+
+ public boolean hasTagKey(final String key) {
+ return getTagWithKey(key) != null;
+ }
+
+ public boolean tagsIdentical(OsmElement other) {
+ if (getTagCnt() != other.getTagCnt())
+ return false;
+ for(Map.Entry entry : tags.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ String othertag = other.getTagWithKey(key);
+ if (othertag == null || value.equalsIgnoreCase(othertag))
+ return false;
+ }
+ return true;
+ }
+
+ public long getOsmId() {
+ return mOsmId;
+ }
+
+ public String toString() {
+ return "OsmId="+mOsmId;
+ }
+
+ void updateVisuals(OsmDB db) {
+ /* See also notes on http://wiki.openstreetmap.org/wiki/The_Future_of_Areas */
+ mIsArea = hasTag("area", "yes") || hasTagKey("landuse") || hasTagKey("building") || hasTagKey("natural");
+ }
+
+ public boolean tagIsFiltered(String key, String value) {
+ //return false;
+ return key.startsWith("kms:") || key.startsWith("osak:") || key.equals("created_by")
+ || key.equals("addr:country") || key.equals("addr:postcode") || key.equals("source");
+ }
+
+ // Number of lines a given text needs given a maximum width
+ protected int getTagTextLines(String text, Paint paint, float maxw, float indentw) {
+ float w = paint.measureText(text);
+ if (w<= maxw)
+ return 1;
+ return 1 + (int)((w-maxw)/(maxw-indentw)); // FIXME: Approximation, needs char algo
+ }
+
+ protected float formatWrappedText(ArrayList lines, String text, Paint paint, float maxw) {
+ float w = paint.measureText(text);
+ if (w <= maxw) {
+ lines.add(text);
+ return w;
+ }
+ while (!text.isEmpty()) {
+ int ch = paint.breakText(text, true, maxw, null);
+ lines.add(text.substring(0, ch));
+ text = text.substring(ch);
+ if (!text.isEmpty())
+ text = "\u21AA "+text;
+ }
+ return maxw;
+ }
+
+ // Special headlines where two tags are combined, currently only highwayType : NameValue
+ protected int headLines() {
+ int lines = 0;
+ if (hasTagKey("highway") && hasTagKey("name"))
+ lines += 1;
+ //if (hasTagKey("amenity") && hasTagKey("name"))
+ // lines += 1;
+ return lines;
+ }
+
+ protected float formatWrappedText(ArrayList lines, Paint paint, float maxw) {
+
+ String head = null;
+ float w = 0.0f;
+
+ // Compact headlines like "ValueOfHighway : ValueOfName"
+ if (hasTagKey("highway") && hasTagKey("name")) {
+ head = getTagWithKey("highway") + " : '" + getTagWithKey("name") + "'";
+ w = formatWrappedText(lines, head, paint, maxw);
+ }
+// if (hasTagKey("amenity") && hasTagKey("name")) {
+// head = getTagWithKey("amenity") + " : '" + getTagWithKey("name") + "'";
+// float tmp = formatWrappedText(lines, head, paint, maxw);
+// if (w entry : tags.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ if (head != null && (key.equalsIgnoreCase("highway") || key.equalsIgnoreCase("name")))
+ continue;
+ if (!tagIsFiltered(key, value)) {
+ String txt = key+" = "+value;
+ float tmp = formatWrappedText(lines, txt, paint, maxw);
+ if (w lines, int idx, Paint paint, float maxw) {
+ //final String sym = "\u21a9"; // bend arrow
+ final String sym = " \u21df";
+ final String spc = " ";
+ String txt = lines.get(idx);
+ float txtw = paint.measureText(txt);
+ float symw = paint.measureText(sym);
+ float spcw = paint.measureText(spc);
+ if (txtw+symw < maxw) {
+ int spcs = (int)((maxw-(txtw+symw))/spcw);
+ for (int ii=0; ii list = new ArrayList();
+ final int inset = 3; // inset relative to viewbox
+ final int xborder = 4; int yborder = 4; // margin between text and surrounding box
+ float x,y, w;
+ int lines;
+ float round = 1.0f;
+ float lspace = pcfg.tag.getTextSize()+1;
+
+ float maxx = viewrect.width() * 0.49f;
+ float maxy = viewrect.height() * 0.49f;
+ float maxw = maxx-2*xborder-inset;
+ int maxl = (int)(maxy/lspace);
+
+ w = formatWrappedText(list, pcfg.tag, maxw);
+ lines = list.size();
+
+ if (lines>maxl) {
+ lines=maxl;
+ clipLine(list, maxl-1, pcfg.tag, maxw);
+ }
+
+ if (xalign == Paint.Align.LEFT) { // x,y is upper left corner of box
+ x = viewrect.left+inset;
+ } else if (xalign == Paint.Align.RIGHT) {
+ x = viewrect.right-w-2*xborder-inset;
+ } else {
+ x = (viewrect.right-viewrect.left)/2-w/2-inset;
+ }
+ if (yalign == Paint.Align.LEFT) {
+ y = viewrect.top+inset;
+ } else if (yalign == Paint.Align.RIGHT) {
+ y = viewrect.bottom-(2*yborder+lines*lspace)-inset;
+ } else {
+ y = (viewrect.bottom-viewrect.top)/2-(lines*lspace)/2-inset;
+ }
+
+ // Draw box
+ RectF rrect = new RectF(x,y,x+w+2*xborder,y+2*yborder+lines*lspace);
+
+ if (headLines() > 0) {
+ drawFancyFrame(canvas, pcfg.tagback, pcfg.tagback3, rrect, lines-1, lspace);
+ } else {
+ canvas.drawRoundRect(rrect, 2*round, 2*round, pcfg.tagback3);
+ }
+ canvas.drawRoundRect(rrect, 2*round, 2*round, pcfg.focus2[style]);
+
+ // Draw text
+ for (int ii=0; ii= e.mCompare) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ public void compareSetDistTo(double lon, double lat_merc) { mCompare = distTo(lon, lat_merc); }
+ public void compareSetAngleTo(double lon, double lat_merc, double offset) { mCompare = angleTo(lon, lat_merc)+offset; }
+ public double compareGet() { return mCompare; }
+}
diff --git a/src/dk/network42/osmfocus/OsmException.java b/src/dk/network42/osmfocus/OsmException.java
new file mode 100644
index 0000000..0d3f69c
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmException.java
@@ -0,0 +1,15 @@
+package dk.network42.osmfocus;
+
+import java.io.IOException;
+
+public class OsmException extends IOException {
+
+ public OsmException(final String string) {
+ super(string);
+ }
+
+ public OsmException(final String string, final Throwable e) {
+ super(string);
+ initCause(e);
+ }
+}
diff --git a/src/dk/network42/osmfocus/OsmNode.java b/src/dk/network42/osmfocus/OsmNode.java
new file mode 100644
index 0000000..e694622
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmNode.java
@@ -0,0 +1,86 @@
+package dk.network42.osmfocus;
+
+import android.graphics.Canvas;
+
+public class OsmNode extends OsmElement {
+ private static final String TAG = "OsmNode";
+
+ int mLon, mLat; // OSM geopoint, WGS84 times 1e7
+ float mLatMerc;
+ public boolean mIsWayNode = false;
+
+ OsmNode(final int lon, final int lat) {
+ super(0, 0);
+ mLon = lon;
+ mLat = lat;
+ mLatMerc = (float) GeoMath.latToMercator(getLat());
+ }
+
+ OsmNode(final long osmId, final long osmVersion, final int lon, final int lat) {
+ super(osmId, osmVersion);
+ mLon = lon;
+ mLat = lat;
+ mLatMerc = (float) GeoMath.latToMercator(getLat());
+ }
+
+ public double getLon() { return ((double)mLon)/1e7; }
+ public double getLat() { return ((double)mLat)/1e7; }
+ public double getLatMerc() { return mLatMerc; }
+ public int getOsmLon() { return mLon; }
+ public int getOsmLat() { return mLat; }
+
+ // For screen mapping when db translation is not active
+ public float getX() { return ((float) getLon())*MapView.prescale; }
+ public float getY() { return mLatMerc*MapView.prescale; }
+
+ // For use when db translation is active
+ public float getX(OsmDB db) { return ((float) ((getLon()-db.mX)*MapView.prescale)); }
+ public float getY(OsmDB db) { return ((float) ((mLatMerc-db.mYmerc)*MapView.prescale)); }
+
+ //static double getLatMercator(final double lat) { return Math.toDegrees(Math.log(Math.tan(Math.PI/4+Math.toRadians(lat)/2))); }
+
+ //public double getLambertLat() {
+ // return ((double)mLat)/1e7;
+ //}
+ //public double getLambertLon() { return ((double)mLon)/1e7; }
+
+ // Lambert projection
+ /*public NodePoint convertToXY(String lat, String lon){
+ float phi = (float) GPS.radians(Float.parseFloat(lat));
+ float lambda = (float) GPS.radians(Float.parseFloat(lon));
+ float q = (float) (2*Math.sin( ((Math.PI/2) - phi)/2 ));
+ float x = (float)(q*Math.sin(lambda));
+ float y = (float)(q*Math.cos(lambda));
+ return new NodePoint(x,y);
+
+ }*/
+
+ public void draw(Canvas canvas, OsmDB db, PaintConfig pcfg) {
+ if (! mIsWayNode) {
+ if (pcfg.getLayer()==1) {
+ //Log.d(TAG, "Node: xy=("+getX(db)+","+getY(db)+
+ // "), latlon=("+getLat()+","+getLon()+
+ // "), DBorig="+db.mX+","+db.mYmerc+
+ // ", scale="+pcfg.scale);
+ ////canvas.drawCircle(getX(db), getY(db), 2.0f/pcfg.scale, pcfg.focus);
+ //canvas.drawPoint(getX(db), getY(db), pcfg.focus);
+ }
+ }
+ }
+
+ public void highlight(Canvas canvas, OsmDB db, PaintConfig pcfg, int style)
+ {
+// Log.d(TAG, "NodeHighlight: xy=("+getX(db)+","+getY(db)+
+// "), latlon=("+getLat()+","+getLon()+
+// "), DBorig="+db.mX+","+db.mYmerc);
+ canvas.drawCircle(getX(db), getY(db), 5.0f*pcfg.densityScale/pcfg.mScale, pcfg.focus[style]);
+ }
+
+ public double distTo(double lon, double lat_merc) {
+ return Math.hypot(getLon()-lon, getLatMerc()-lat_merc);
+ }
+
+ public double angleTo(double lon, double lat_merc) {
+ return Math.atan2(getLatMerc()-lat_merc, getLon()-lon);
+ }
+}
diff --git a/src/dk/network42/osmfocus/OsmParseException.java b/src/dk/network42/osmfocus/OsmParseException.java
new file mode 100644
index 0000000..6ae0522
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmParseException.java
@@ -0,0 +1,8 @@
+package dk.network42.osmfocus;
+
+public class OsmParseException extends Exception {
+
+ public OsmParseException(final String msg) {
+ super(msg);
+ }
+}
diff --git a/src/dk/network42/osmfocus/OsmParser.java b/src/dk/network42/osmfocus/OsmParser.java
new file mode 100644
index 0000000..0084768
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmParser.java
@@ -0,0 +1,171 @@
+package dk.network42.osmfocus;
+
+import java.util.ArrayList;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.helpers.DefaultHandler;
+
+import android.util.Log;
+
+public class OsmParser extends DefaultHandler {
+ private static final String TAG = "OsmParser";
+ private OsmDB mDb;
+ private OsmNode mCurrentNode;
+ private OsmWay mCurrentWay;
+ private OsmRelation mCurrentRel;
+ private OsmBounds mCurrentBound;
+ private final ArrayList exceptions;
+
+ public OsmParser() {
+ super();
+ mDb = new OsmDB();
+ mCurrentNode = null;
+ mCurrentWay = null;
+ mCurrentRel = null;
+ mCurrentBound = null;
+ exceptions = new ArrayList();
+ }
+
+ public OsmDB getStorage() {
+ return mDb;
+ }
+
+ public void startElement(final String uri, final String name, final String qName, final Attributes atts) {
+ //Log.d(TAG, "startElement(), name="+name);
+ try {
+ if (isOsmElement(name)) {
+ parseOsmElement(name, atts);
+ } else if (isWayNode(name)) {
+ parseWayNode(atts);
+ } else if (isTag(name)) {
+ parseTag(atts);
+ } else if (isRelationMember(name)) {
+ parseRelationElement(atts);
+ } else if (isBounds(name)) {
+ parseBounds(atts);
+ }
+ } catch (OsmParseException e) {
+ Log.e(TAG, "OsmParseException", e);
+ exceptions.add(e);
+ }
+ }
+
+ public void endElement(final String uri, final String name, final String qName) {
+ //Log.d(TAG, "endElement(), name="+name);
+ if (isNode(name)) {
+ mDb.insertNode(mCurrentNode);
+ mCurrentNode = null;
+ } else if (isWay(name)) {
+ mDb.insertWay(mCurrentWay);
+ mCurrentWay = null;
+ } else if (isRelation(name)) {
+ mDb.insertRelation(mCurrentRel);
+ mCurrentRel = null;
+ } else if (isBounds(name)) {
+ mDb.insertBounds(mCurrentBound);
+ mCurrentBound = null;
+ }
+ }
+
+ private void parseOsmElement(final String name, final Attributes atts) throws OsmParseException {
+ try {
+ long osmId = Long.parseLong(atts.getValue("id"));
+ long osmVersion = Long.parseLong(atts.getValue("version"));
+
+ if (isNode(name)) {
+ int lon = (int) (Double.parseDouble(atts.getValue("lon")) * 1E7);
+ int lat = (int) (Double.parseDouble(atts.getValue("lat")) * 1E7);
+ mCurrentNode = mDb.createNode(osmId, osmVersion/*, status*/, lon, lat);
+ //Log.d(TAG, "Node: id="+osmId+" pos="+lon+","+lat);
+ } else if (isWay(name)) {
+ mCurrentWay = mDb.createWay(osmId, osmVersion/*, status*/);
+ //Log.d(TAG, "Way: id="+osmId);
+ } else if (isRelation(name)) {
+ mCurrentRel = mDb.createRelation(osmId, osmVersion/*, status*/);
+ //Log.d(TAG, "Relation: id="+osmId);
+ }
+ } catch (NumberFormatException e) {
+ throw new OsmParseException("Element unparsable");
+ }
+ }
+
+ private void parseBounds(final Attributes atts) throws OsmParseException {
+ try {
+ int minlat = (int) (Double.parseDouble(atts.getValue("minlat")) * 1E7);
+ int maxlat = (int) (Double.parseDouble(atts.getValue("maxlat")) * 1E7);
+ int minlon = (int) (Double.parseDouble(atts.getValue("minlon")) * 1E7);
+ int maxlon = (int) (Double.parseDouble(atts.getValue("maxlon")) * 1E7);
+ try {
+ mCurrentBound = mDb.createBounds(minlon, minlat, maxlon, maxlat);
+ } catch (OsmException e) {
+ throw new OsmParseException("Bounds are not correct");
+ }
+ } catch (NumberFormatException e) {
+ throw new OsmParseException("Bounds unparsable");
+ }
+ }
+
+ private void parseTag(final Attributes atts) {
+ OsmElement currentOsmElement = getCurrentOsmElement();
+ if (currentOsmElement == null) {
+ Log.e(TAG, "Parsing Error: no mCurrentOsmElement set!");
+ } else {
+ String k = atts.getValue("k");
+ String v = atts.getValue("v");
+ currentOsmElement.addOrUpdateTag(k, v);
+ //Log.d(TAG, "Tag, "+k+"="+v);
+ }
+ }
+
+ private void parseWayNode(final Attributes atts) throws OsmParseException {
+ try {
+ if (mCurrentWay == null) {
+ Log.e(TAG, "No mCurrentWay set!");
+ } else {
+ long nodeOsmId = Long.parseLong(atts.getValue("ref"));
+ OsmNode node = mDb.getNode(nodeOsmId);
+ mCurrentWay.addNode(node);
+ }
+ } catch (NumberFormatException e) {
+ throw new OsmParseException("WayNode unparsable");
+ }
+ }
+
+ private void parseRelationElement(final Attributes atts) throws OsmParseException {
+ try {
+ if (mCurrentRel == null) {
+ Log.e(TAG, "No mCurrentRel set!");
+ } else {
+ String type = atts.getValue("type");
+ String role = atts.getValue("role");
+ long ref = Long.parseLong(atts.getValue("ref"));
+ mCurrentRel.addMember(type, role, ref, mDb.getElement(ref));
+ //Log.d(TAG, "Rel, type="+type+" role="+role+" ref="+ref);
+ }
+ } catch (NumberFormatException e) {
+ throw new OsmParseException("RelationElement unparsable");
+ }
+ }
+
+ private OsmElement getCurrentOsmElement() {
+ if (mCurrentNode != null) {
+ return mCurrentNode;
+ }
+ if (mCurrentWay != null) {
+ return mCurrentWay;
+ }
+ if (mCurrentRel != null) {
+ return mCurrentRel;
+ }
+ return null;
+ }
+
+ private static boolean isNode(final String name) { return name.equalsIgnoreCase("node"); }
+ private static boolean isWay(final String name) { return name.equalsIgnoreCase("way"); }
+ private static boolean isWayNode(final String name) { return name.equalsIgnoreCase("nd"); }
+ private static boolean isBounds(final String name) { return name.equalsIgnoreCase("bounds"); }
+ private static boolean isRelation(final String name) { return name.equalsIgnoreCase("relation"); }
+ private static boolean isRelationMember(final String name) { return name.equalsIgnoreCase("member"); }
+ private static boolean isTag(final String name) { return name.equalsIgnoreCase("tag"); }
+ private static boolean isOsmElement(final String name) { return isNode(name) || isWay(name) || isRelation(name); }
+}
diff --git a/src/dk/network42/osmfocus/OsmRelation.java b/src/dk/network42/osmfocus/OsmRelation.java
new file mode 100644
index 0000000..24de252
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmRelation.java
@@ -0,0 +1,143 @@
+package dk.network42.osmfocus;
+
+import java.util.ArrayList;
+
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.util.Log;
+
+public class OsmRelation extends OsmElement {
+ private static final String TAG = "OsmRelation";
+ private class RelMember {
+ String mRole;
+ String mType;
+ long mRef;
+ OsmElement mElem;
+ boolean mRender;
+ }
+ protected final ArrayList mElems;
+ protected Path mPath = new Path();
+ boolean mDrawInners = false; // Set if inners have tags different from relation/outer
+ OsmElement mVisualDefiningElem; // Normally relation, but may be outer way
+
+ OsmRelation(final long osmId, final long osmVersion) {
+ super(osmId, osmVersion);
+ mElems = new ArrayList();
+ }
+
+ boolean hasRole(String role) {
+ for (RelMember member: mElems) {
+ if (member.mRole.equalsIgnoreCase(role)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ long getRoleRef(String role) {
+ for (RelMember member: mElems) {
+ if (member.mRole.equalsIgnoreCase(role)) {
+ if (member.mElem == null) {
+ Log.d(TAG, "Relation w NULL elem, ref="+member.mRef);
+ return 0; // FIXME
+ }
+ return member.mElem.getOsmId();
+ }
+ }
+ return 0; // FIXME
+ }
+
+ void addMember(final String type, final String role, long ref, OsmElement e) {
+ RelMember elem = new RelMember();
+ elem.mRole = role;
+ elem.mType = type;
+ elem.mElem = e;
+ elem.mRef = ref;
+ elem.mRender = false;
+ mElems.add(elem);
+ }
+
+ /* Detect which object define how the relation should be rendered.
+ * In principle tags should be on relations, but sometimes they are
+ * on the outer element(s) */
+ void updateVisualDefiningElem() {
+ if (!hasTag("type", "multipolygon")) {
+ Log.d(TAG, "Relation "+mOsmId+" is not a multipolygon");
+ return;
+ }
+ mVisualDefiningElem = this;
+ for (RelMember member: mElems) {
+ if (member.mRole.equalsIgnoreCase("outer") &&
+ member.mElem != null &&
+ member.mElem.getTagCnt() > 0 &&
+ getTagCnt() == 1) {
+ // If relation only has one tag ('type') and we have an outer
+ // with tags, use the outer for defining the visuals
+ mVisualDefiningElem = member.mElem;
+ }
+ }
+ for (RelMember member: mElems) {
+ // If we have an inner that differs from the element defining
+ // the visuals of the relation, then we render it
+ if (member.mRole.equalsIgnoreCase("inner") &&
+ member.mElem != null &&
+ member.mElem.getTagCnt()>0 &&
+ !mVisualDefiningElem.tagsIdentical(member.mElem)) {
+ mDrawInners = true;
+ member.mRender = true;
+ }
+ }
+ }
+
+ void updateVisuals(OsmDB db) {
+ if (!hasTag("type", "multipolygon")) {
+ Log.d(TAG, "Relation "+mOsmId+" is not a multipolygon");
+ return;
+ }
+ super.updateVisuals(db);
+ mPath.rewind();
+ mPath.setFillType(Path.FillType.EVEN_ODD);
+ for (RelMember member: mElems) {
+ OsmWay way = (OsmWay) member.mElem;
+ if (way != null) {
+ if (this.mOsmId==446026) { Log.d(TAG, "Add Path "+way.mOsmId); }
+ mPath.addPath(way.mPath); // FIXME: Polygons may be build from several ways
+ } else {
+ Log.d(TAG, "Relation w id="+mOsmId+" is missing way with id="+member.mRef+" for role="+member.mRole);
+ }
+ }
+ updateVisualDefiningElem();
+ }
+
+ public void draw(Canvas canvas, OsmDB db, PaintConfig pcfg) {
+ //Log.d(TAG, "draw Relation: id="+mOsmId);
+ if (!pcfg.wireframe) {
+ pcfg.getWayPaints(this, mVisualDefiningElem);
+ if (pcfg.look != null) {
+ canvas.drawPath(mPath, pcfg.look);
+ }
+ if (pcfg.look2 != null) {
+ canvas.drawPath(mPath, pcfg.look2);
+ }
+// if (pcfg.nodelook != null) {
+// for (int ii = 0, size = mNodes.size(); ii= Build.VERSION_CODES.KITKAT) {
+// return getBitmap().getAllocationByteCount();
+// } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
+// return bm.getRowBytes() * bm.getHeight();
+// }
+ }
+
+ public boolean canDraw(Canvas canvas, PaintConfig pcfg) {
+ if (mBitmap != null) {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean draw(Canvas canvas, PaintConfig pcfg) {
+ if (mBitmap != null) {
+ //Log.d(TAG, "Draw bitmap at "+mBox2+" ("+mViewBox+") zoom="+mZoom);
+ canvas.drawBitmap(mBitmap, null, mViewBox, pcfg.paint);
+ return true;
+ } else if (mDownloadErrs>0) {
+ drawError(canvas, pcfg);
+ }
+ return false;
+ }
+
+ public void drawX(Canvas canvas, PaintConfig pcfg) {
+ canvas.drawLine(mViewBox.left, mViewBox.bottom, mViewBox.right, mViewBox.top, pcfg.paint);
+ canvas.drawLine(mViewBox.left, mViewBox.top, mViewBox.right, mViewBox.bottom, pcfg.paint);
+ }
+
+ public void drawP(Canvas canvas, PaintConfig pcfg) {
+ float xc = (mViewBox.left+mViewBox.right)/2;
+ float yc = (mViewBox.bottom+mViewBox.top)/2;
+ canvas.drawLine(mViewBox.left, yc, mViewBox.right, yc, pcfg.paint);
+ canvas.drawLine(xc, mViewBox.top, xc, mViewBox.bottom, pcfg.paint);
+ }
+
+ public void drawFade(Canvas canvas, PaintConfig pcfg) {
+ canvas.drawRect(mViewBox, pcfg.fade);
+ }
+
+ public void drawError(Canvas canvas, PaintConfig pcfg) {
+ canvas.drawRect(mViewBox, pcfg.tileerr);
+ }
+
+ // public void drawText(Canvas canvas, Paint paint, String st) {
+// float w = paint.measureText(st);
+// canvas.drawText(st, mViewBox.centerX(), mViewBox.centerY(), paint);
+// }
+
+ public void setQueuedForDownload() {
+ assert(mDownloadOngoing==false);
+ mDownloadOngoing = true;
+ }
+
+ public void clearQueuedForDownload() {
+ assert(mDownloadOngoing==true);
+ mDownloadOngoing = false;
+ }
+
+ public boolean isQueuedForDownload() {
+ return mDownloadOngoing;
+ }
+
+ public boolean download(String useragent, String provider) {
+ Bitmap bm = dlFromUrl(tileUrl(provider));
+ if (bm != null) {
+ mBitmap = flipBitmap(bm);
+ //Log.d(TAG, "Download done");
+ return true;
+ } else {
+ Log.e(TAG, "Download failed, tile="+this);
+ return false;
+ }
+ }
+
+ private Bitmap dlFromUrl(String tileurl) {
+ try {
+ //Log.d(TAG, "Loading tile "+tileurl);
+ URL url = new URL(tileurl);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setDoInput(true);
+ //Log.d(TAG, "Connect for tile download");
+ conn.connect();
+ InputStream input = conn.getInputStream();
+ //Log.d(TAG, "Decode bitmap");
+ Bitmap bm = BitmapFactory.decodeStream(input);
+ //Log.d(TAG, "Downloaded tile "+tileurl+", bbox="+mBox.toString());
+ return bm;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private Bitmap flipBitmap(Bitmap bm) {
+ Matrix m = new Matrix();
+ m.preScale(1, -1);
+ Bitmap bmf = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), m, false);
+ if (bmf!=null)
+ bmf.setDensity(DisplayMetrics.DENSITY_DEFAULT);
+ return bmf;
+ }
+
+ static public int lon2tile(double lon, int zoom) {
+ int xtile = (int)Math.floor((lon + 180.0)/360.0 * (1<= (1<= (1<=vrect.left && xt <= vrect.right && yt >= vrect.top && yt <= vrect.bottom)
+ drawTile(canvas, pcfg, xt, yt, zoom, layer);
+ }
+ for (yt=cy-(cc-1); yt<=cy+cc-1; yt++) {
+ xt = cx+cc;
+ //Log.d(TAG, "2: "+xt+","+yt);
+ if (xt>=vrect.left && xt <= vrect.right && yt >= vrect.top && yt <= vrect.bottom)
+ drawTile(canvas, pcfg, xt, yt, zoom, layer);
+ }
+ for (xt=cx+cc; xt>=cx-cc; xt--) {
+ yt = cy+cc;
+ //Log.d(TAG, "3: "+xt+","+yt);
+ if (xt>=vrect.left && xt <= vrect.right && yt >= vrect.top && yt <= vrect.bottom)
+ drawTile(canvas, pcfg, xt, yt, zoom, layer);
+ }
+ for (yt=cy+cc-1; yt>=cy-(cc-1); yt--) {
+ xt = cx-cc;
+ //Log.d(TAG, "4: "+xt+","+yt);
+ if (xt>=vrect.left && xt <= vrect.right && yt >= vrect.top && yt <= vrect.bottom)
+ drawTile(canvas, pcfg, xt, yt, zoom, layer);
+ }
+ }
+ //Log.d(TAG, "Tile layer draw finished ");
+ }
+ public void drawLegends(Canvas canvas, RectF viewrect, PaintConfig pcfg) {
+ if (mAttrib != null) {
+ canvas.drawText(mAttrib, viewrect.centerX()-pcfg.attrib.measureText(mAttrib)/2,
+ viewrect.centerY()+viewrect.height()*0.4f, pcfg.attrib);
+ }
+ }
+
+ protected abstract String getTileId(int xt, int yt, int zoom);
+
+ protected abstract OsmTile createTile(int xt, int yt, int zoom);
+
+ protected abstract void handleMissingTile(OsmTile t, int layer);
+
+ protected void drawTile(Canvas canvas, PaintConfig pcfg, int xt, int yt, int zoom, int layer) {
+ //Log.d(TAG, "Draw tile: "+xt+","+yt+" zoom="+zoom);
+ String tid = getTileId(xt, yt, zoom);
+ OsmTile t = mTiles.get(tid);
+ if (t == null) {
+ t = createTile(xt, yt, zoom);
+ mTiles.put(tid, t);
+ //Log.d(TAG, "New tile, TID="+tid+" LruCache size="+mTiles.size());
+ handleMissingTile(t, layer);
+ t.drawX(canvas, pcfg);
+ } else {
+ if (! t.draw(canvas, pcfg)) {
+ t.drawX(canvas, pcfg);
+ }
+ }
+ }
+
+// private void drawTileCoords(Canvas canvas, PaintConfig pcfg, OsmTile t) {
+// String st = "";
+// st.format("%d,%d,%d", t.mX, t.mY, t.mZoom);
+// t.drawText(canvas, pcfg.paint, st);
+// }
+
+ protected abstract void downloadTile(OsmTile t);
+
+ protected int getZoom(PaintConfig pcfg) {
+ return view2OsmZoom(pcfg.mScale);
+ }
+
+ // Convert from internal view scale value to OSM zoom levels
+ protected int view2OsmZoom(float scale) {
+ int zoom = (int) (16+(1.0+Math.log((double)scale)/Math.log(2.0)));
+ if (zoom > OsmTile.MAX_ZOOM)
+ zoom = OsmTile.MAX_ZOOM;
+ //Log.d(TAG, "Scale="+scale+" OsmZoom="+zoom);
+ return zoom;
+ }
+
+ public void onTrimMemory(int level) {
+ if (level == ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+ mTiles.trimToSize(mTiles.maxSize()/2);
+ } else if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
+ mTiles.trimToSize(0);
+ }
+ }
+
+ public int getMemorySize() {
+ return mTiles.size();
+ }
+
+ public void flushCache() {
+ mTiles.evictAll();
+ }
+
+ protected int getNumLayers() {
+ return 1;
+ }
+
+ // Attribute, like OSM data attribution
+ public void setAttrib(String attrib) {
+ mAttrib = attrib;
+ }
+
+ protected class TileLruCache extends LruCache {
+ public TileLruCache(int maxSize) {
+ super(maxSize);
+ }
+ protected int sizeOf(String k, OsmTile v) {
+ return v.getByteSize();
+ }
+ };
+}
diff --git a/src/dk/network42/osmfocus/OsmTileLayerBm.java b/src/dk/network42/osmfocus/OsmTileLayerBm.java
new file mode 100644
index 0000000..b00d453
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmTileLayerBm.java
@@ -0,0 +1,35 @@
+package dk.network42.osmfocus;
+
+import android.view.View;
+
+public class OsmTileLayerBm extends OsmTileLayer {
+ private static final String TAG = "OsmTileLayerBm";
+ String mProviderArg = "";
+
+ public OsmTileLayerBm(OsmTileProvider provider, int maxCacheSize) {
+ super(provider, maxCacheSize);
+ //mProviderArg = "http://a.tile.openstreetmap.org";
+ //mProviderArg = "http://a.tile.opencyclemap.org/cycle";
+ }
+
+ public void setProviderUrl(String url) {
+ mProviderArg = url;
+ flushCache();
+ }
+
+ protected OsmTile createTile(int xt, int yt, int zoom) {
+ return new OsmTile(xt, yt, zoom);
+ }
+
+ protected void handleMissingTile(OsmTile t, int layer) {
+ downloadTile(t);
+ }
+
+ protected void downloadTile(OsmTile t) {
+ mProvider.downloadTile(mProviderArg, t, mHandler);
+ }
+
+ protected String getTileId(int xt, int yt, int zoom) {
+ return OsmTile.tileId(mProviderArg, xt, yt, zoom);
+ }
+}
diff --git a/src/dk/network42/osmfocus/OsmTileLayerVector.java b/src/dk/network42/osmfocus/OsmTileLayerVector.java
new file mode 100644
index 0000000..016400c
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmTileLayerVector.java
@@ -0,0 +1,221 @@
+package dk.network42.osmfocus;
+
+import java.util.ArrayList;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.View;
+
+public class OsmTileLayerVector extends OsmTileLayer {
+ private static final String TAG = "OsmTileLayerVector";
+ private static final int TILEZOOM = 16; // Vector tile zoom level
+ private static final int MAX_POI = 8;
+ private static final float MIN_SCALE = 0.5f;
+ String mProviderArg;
+ SharedData mG;
+ ArrayList mNearest = new ArrayList(MAX_POI);
+ RectF [] mUsedArea = new RectF[MAX_POI];
+ boolean filterWayNodes = false;
+
+ public OsmTileLayerVector(OsmTileProvider provider, int maxCacheSize) {
+ super(provider, maxCacheSize);
+ mProviderArg = "";
+ }
+
+ public void setSharedData(SharedData data) {
+ mG = data;
+ }
+
+ protected void tryAutoDownload() {
+ if (mG.mVectorAutoDownload == SharedData.AUTODOWNLOAD_MANUAL)
+ return;
+
+ int xt = OsmTile.lon2tile(mG.mLon, TILEZOOM);
+ int yt = OsmTile.lat2tile(mG.mLat, TILEZOOM);
+ int x, y, c=0;
+ String tid;
+
+ if (mG.mVectorAutoDownload == SharedData.AUTODOWNLOAD_AUTOMATIC2)
+ c=1;
+
+ for (x=xt-c; x<=xt+c; x++) {
+ for (y=yt-c; y<=yt+c; y++) {
+ tid = OsmTile.tileId(mProviderArg, x, y, TILEZOOM);
+ OsmTileVector tile = (OsmTileVector) mTiles.get(tid);
+ if (tile != null && tile.mDb == null && !tile.isQueuedForDownload()) {
+ downloadTile(tile);
+ }
+ }
+ }
+ }
+
+ protected OsmTile createTile(int xt, int yt, int zoom) {
+ return new OsmTileVector(xt, yt, zoom);
+ }
+
+ // Called once immediately after createTile()
+ protected void handleMissingTile(OsmTile t, int layer) {
+ tryAutoDownload();
+ }
+
+ // Shading are only drawn on layer 1
+ protected void drawTile(Canvas canvas, PaintConfig pcfg, int xt, int yt, int zoom, int layer) {
+ //Log.d(TAG, "Draw tile: "+xt+","+yt+" zoom="+zoom);
+ String tid = getTileId(xt, yt, zoom);
+ OsmTile t = mTiles.get(tid);
+ if (t == null) {
+ t = createTile(xt, yt, zoom);
+ mTiles.put(tid, t);
+ //Log.d(TAG, "New tile, TID="+tid+" LruCache size="+mTiles.size());
+ handleMissingTile(t, layer);
+ if (layer==1)
+ t.drawX(canvas, pcfg);
+ } else {
+ if (layer==0) {
+ t.draw(canvas, pcfg);
+ }
+ if (layer==1 && !t.canDraw(canvas, pcfg)) {
+ t.drawX(canvas, pcfg);
+ }
+ }
+ }
+
+ protected void downloadTile(OsmTile t) {
+ if (t.mDownloadErrs < 2) {
+ mProvider.downloadTile(mProviderArg, t, mHandler);
+ } else {
+ //Log.d(TAG, "Skipping download, errs="+t.mDownloadErrs);
+ }
+ }
+
+ protected String getTileId(int xt, int yt, int zoom) {
+ return OsmTile.tileId(mProviderArg, xt, yt, zoom);
+ }
+
+ // See: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
+ @Override
+ protected int view2OsmZoom(float scale) {
+ return 16;
+ }
+
+ public void download(double lon, double lat) {
+ int xt = OsmTile.lon2tile(lon, TILEZOOM);
+ int yt = OsmTile.lat2tile(lat, TILEZOOM);
+ String tid = OsmTile.tileId(mProviderArg, xt, yt, TILEZOOM);
+ OsmTile t = mTiles.get(tid);
+ if (t == null) {
+ t = createTile(xt, yt, TILEZOOM);
+ }
+ downloadTile(t);
+ }
+
+ public int closestElements(double lon, double lat, int max, ArrayList near) {
+
+ near.clear();
+ int xt = OsmTile.lon2tile(lon, TILEZOOM);
+ int yt = OsmTile.lat2tile(lat, TILEZOOM);
+ int x, y, c=1;
+ String tid;
+
+ for (x=xt-c; x<=xt+c; x++) {
+ for (y=yt-c; y<=yt+c; y++) {
+ tid = OsmTile.tileId(mProviderArg, x, y, TILEZOOM);
+ OsmTileVector t = (OsmTileVector) mTiles.get(tid);
+ if (t != null && t.mDb != null) {
+ t.mDb.closestElements(lon, lat, max, near);
+ }
+ }
+ }
+ return near.size();
+ }
+
+ public void draw(Canvas canvas, RectF worldport, PaintConfig pcfg) {
+
+ if (pcfg.mScale 0) {
+ txt = mG.mCtx.getString(R.string.info_errdownloadarea);
+ } else {
+ if (mG.mVectorAutoDownload==SharedData.AUTODOWNLOAD_MANUAL) {
+ txt = mG.mCtx.getString(R.string.info_locationoutside);
+ } else {
+ txt = mG.mCtx.getString(R.string.info_downloading);
+ }
+ }
+ canvas.drawText(txt, viewrect.centerX()-mG.mPcfg.debughud.measureText(txt)/2,
+ viewrect.centerY()-mG.mPcfg.mHUDspacing, mG.mPcfg.debughud);
+ }
+ }
+
+ protected int getNumLayers() {
+ return 2; // Vector and shaded
+ }
+
+}
diff --git a/src/dk/network42/osmfocus/OsmTileProvider.java b/src/dk/network42/osmfocus/OsmTileProvider.java
new file mode 100644
index 0000000..7c5fbd7
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmTileProvider.java
@@ -0,0 +1,119 @@
+package dk.network42.osmfocus;
+
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import android.os.Handler;
+import android.util.Log;
+
+public class OsmTileProvider {
+ static private final String TAG = "OsmTileProvider";
+
+ static final int TASK_DOWNLOAD_START = 1;
+ static final int TASK_DOWNLOAD_FAILED = 2;
+ static final int TASK_DOWNLOAD_COMPLETE = 3;
+
+ static final int MAX_THREADS = 4;
+
+ final int KEEP_ALIVE_TIME = 1;
+ final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
+
+ private BlockingQueue mWorkQueue;
+ private ThreadPoolExecutor mTrdPool;
+ private Queue mTaskQueue;
+ private String mUserAgent;
+
+ OsmTileProvider(String agent) {
+ this(agent, Math.min(Runtime.getRuntime().availableProcessors(), MAX_THREADS));
+ }
+
+ OsmTileProvider(String agent, int maxTrds) {
+ mUserAgent = new String(agent);
+ final int trds = Math.min(maxTrds, Runtime.getRuntime().availableProcessors());
+ //Log.d(TAG, "Threads="+trds);
+ mWorkQueue = new LinkedBlockingQueue();
+ mTaskQueue = new LinkedBlockingQueue();
+ mTrdPool = new ThreadPoolExecutor(trds, trds, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mWorkQueue);
+ }
+
+ public int getActiveDownloads() {
+ return mTrdPool.getActiveCount();
+ }
+
+ public void clearPending() {
+ synchronized (this) {
+ //Log.d(TAG, "clearPending start size="+mWorkQueue.size());
+ OsmTileDownloader[] pendingArray = new OsmTileDownloader[mWorkQueue.size()];
+ mWorkQueue.toArray(pendingArray);
+ int len = pendingArray.length;
+ //Log.d(TAG, "ArrayLen="+len);
+ for (int ii = 0; ii < len; ii++) {
+ //Log.d(TAG, "dlTask="+pendingArray[ii]);
+ Thread trd = pendingArray[ii].mThread;
+ if (trd != null) {
+ trd.interrupt();
+ }
+ }
+ }
+ //Log.d(TAG, "clearPending done");
+ }
+
+ public void cancelDownload(OsmTile tile) {
+ synchronized (this) {
+ OsmTileDownloader[] pendingArray = new OsmTileDownloader[mWorkQueue.size()];
+ mWorkQueue.toArray(pendingArray);
+ int len = pendingArray.length;
+ for (int ii = 0; ii < len; ii++) {
+ if (pendingArray[ii].mTile == tile) {
+ Thread trd = pendingArray[ii].mThread;
+ if (trd != null) {
+ trd.interrupt();
+ }
+ }
+ }
+ }
+ }
+
+ public OsmTileDownloader downloadTile(final String providerArg, OsmTile tile, Handler h) {
+
+ tile.setQueuedForDownload();
+ OsmTileDownloader task = mTaskQueue.poll();
+ if (task == null) {
+ task = this.new OsmTileDownloader();
+ }
+ task.mProv = this;
+ task.mProviderArg = providerArg;
+ task.mTile = tile;
+ task.mHandler = h;
+ mTrdPool.execute(task);
+ return task;
+ }
+
+ protected class OsmTileDownloader implements Runnable {
+ OsmTileProvider mProv;
+ String mProviderArg;
+ OsmTile mTile;
+ Thread mThread;
+ Handler mHandler;
+ public void run() {
+ mThread = Thread.currentThread();
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ //Log.d(TAG, "Task started, tile="+mTile);
+ mHandler.obtainMessage(TASK_DOWNLOAD_START, mTile).sendToTarget();
+ if (!mTile.download(mUserAgent, mProviderArg)) {
+ mTile.mDownloadErrs++;
+ }
+ // Mark done before new task starts - see OsmTileProvider.cancelDownload()
+ synchronized (mProv) {
+ mThread = null;
+ }
+ mTile.clearQueuedForDownload();
+ //Log.d(TAG, "Task ended, tile="+mTile);
+ //Log.d(TAG, "Sending TASK_DOWNLOAD_COMPLETE");
+ mHandler.obtainMessage(TASK_DOWNLOAD_COMPLETE, mTile).sendToTarget();
+ }
+ }
+}
diff --git a/src/dk/network42/osmfocus/OsmTileVector.java b/src/dk/network42/osmfocus/OsmTileVector.java
new file mode 100644
index 0000000..695a35c
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmTileVector.java
@@ -0,0 +1,95 @@
+package dk.network42.osmfocus;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Calendar;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import android.graphics.Canvas;
+import android.util.Log;
+
+public class OsmTileVector extends OsmTile {
+ private static final String TAG = "OsmTileVector";
+ OsmDB mDb = null;
+
+ public OsmTileVector(int x, int y, int zoom) {
+ super(x, y, zoom);
+ }
+
+ public boolean canDraw(Canvas canvas, PaintConfig pcfg) {
+ return (mDb != null);
+ }
+
+ public boolean draw(Canvas canvas, PaintConfig pcfg) {
+ if (mDb != null) {
+ //Log.d(TAG, "Draw vector tile at "+mBox2+" ("+mViewBox+") "+mDb);
+ if (pcfg.wireframe || pcfg.mBackMapType == MapLayer.MAPTYPE_INTERNAL) {
+ mDb.draw(canvas, mViewBox, pcfg);
+ }
+ return true;
+ } else if (mDownloadErrs>0) {
+ drawError(canvas, pcfg);
+ }
+ return false;
+ }
+
+ public void drawX(Canvas canvas, PaintConfig pcfg) {
+ drawFade(canvas, pcfg);
+ }
+
+ public boolean download(String useragent, String provider) {
+ //Log.d(TAG, "Download vector");
+ //Log.d(TAG, "Acquire Osm server stream, box="+mBox);
+ try {
+ InputStream istream = OsmServer.getStreamForArea(null, useragent, mBox);
+ OsmDB db = loadData(istream, mBox.toString());
+ if (db != null) {
+ db.updateAndVacuum();
+ mDb = db;
+ Log.d(TAG, "Read db tile: "+mBox+" TID="+this);
+ return true;
+ } else {
+ Log.e(TAG, "Reading of db tile failed "+mBox);
+ }
+ } catch (OsmServerException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (ParserConfigurationException pce) {
+ Log.e(TAG, "SAX parse error", pce);
+ } catch (SAXException se) {
+ Log.e(TAG, "SAX error", se);
+ }
+ return false;
+ }
+
+
+ protected OsmDB loadData(InputStream istream, String info) throws ParserConfigurationException, SAXException, IOException {
+ String start = java.text.DateFormat.getDateTimeInstance().format(Calendar.getInstance().getTime());
+ //Log.d(TAG, "Start download at "+start+", info="+info);
+ SAXParserFactory spf = SAXParserFactory.newInstance();
+ SAXParser sp = spf.newSAXParser();
+ XMLReader xr = sp.getXMLReader();
+ OsmParser parser = new OsmParser();
+ xr.setContentHandler(parser);
+ xr.parse(new InputSource(istream));
+ String end = java.text.DateFormat.getDateTimeInstance().format(Calendar.getInstance().getTime());
+ //Log.d(TAG, "Download finished at "+end+", info="+info);
+ OsmDB db = parser.getStorage();
+ return db;
+// db.updateAndVacuum();
+// db.merge(mG.mDb);
+// String enddb = java.text.DateFormat.getDateTimeInstance().format(Calendar.getInstance().getTime());
+// Log.d(TAG, "Database merged at "+enddb+", info="+info);
+// mG.mDb = db;
+// mapView.postInvalidate();
+ }
+
+}
diff --git a/src/dk/network42/osmfocus/OsmWay.java b/src/dk/network42/osmfocus/OsmWay.java
new file mode 100644
index 0000000..0eeba63
--- /dev/null
+++ b/src/dk/network42/osmfocus/OsmWay.java
@@ -0,0 +1,138 @@
+package dk.network42.osmfocus;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.Log;
+
+public class OsmWay extends OsmElement {
+ private static final String TAG = "OsmWay";
+
+ protected final ArrayList mNodes;
+ protected boolean mIsClosed = false;
+ protected Path mPath = new Path();
+ double mLonClose, mLatMercClose;
+ int mCloseStartNode;
+ boolean mIsMultipolyOuter = false; // Drawn as relation multipolygon instead if true
+
+ OsmWay(final long osmId, final long osmVersion) {
+ super(osmId, osmVersion);
+ mNodes = new ArrayList();
+ }
+
+ void addNode(final OsmNode node) {
+ //Log.d(TAG, "id="+mOsmId+": ("+node.getLat()+","+node.getLon()+"), nodes="+mNodes.size());
+ //mPath.moveTo(node.getX(), node.getY());
+ mNodes.add(node);
+ }
+
+ public List getNodes() {
+ return mNodes;
+ }
+
+ // For screen mapping when db translation is not active
+ public float getX() { return ((float) mLonClose*MapView.prescale); }
+ public float getY() { return ((float) mLatMercClose*MapView.prescale); }
+
+ public double distTo(double lon, double lat_merc) {
+ int size = mNodes.size();
+ double dd, dst = Double.POSITIVE_INFINITY;
+ for (int ii=0; ii= Configuration.SCREENLAYOUT_SIZE_LARGE;
+ boolean landscape = (ctx.getResources().getConfiguration().orientation & Configuration.ORIENTATION_LANDSCAPE) != 0;
+ //Log.d(TAG, "Pref:POI num=" + numpoi+" isLargeScreen="+largescreen+" landscape="+landscape);
+ if (numpoi == 0) { // Auto
+ if (largescreen || landscape) {
+ mPoisToShow = 8;
+ } else {
+ mPoisToShow = 4;
+ }
+ Log.d(TAG, "Auto POI num=" + mPoisToShow);
+ } else {
+ mPoisToShow = numpoi;
+ }
+
+ int backtype = Integer.parseInt(sharedPrefs.getString("pref_backmaptype", "1"));
+ if (mPcfg.mBackMapType != backtype) {
+ mPcfg.mBackMapType = backtype;
+ mTileLayer.setProviderUrl(OsmTileLayer.urlFromType(mPcfg.mBackMapType));
+ }
+ mPcfg.update(ctx);
+ }
+
+ public void printState(PrintWriter pw) {
+ pw.print("Loc="+mLon+","+mLat);
+ pw.print("PhyLoc="+mPhyLocation);
+ }
+
+ static void checkAppUpdate(Context ctx) {
+// SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+// int ver = getAppVersion(ctx);
+// int prefver = prefs.getInt(PREF_VER_KEY, 0);
+// SharedPreferences.Editor ed = prefs.edit();
+// if (prefver >= 0) {
+// if (ver==2) {
+// Log.d(TAG, "Do pref update, have "+prefver+" wants "+ver);
+// ed.remove("pref_autoload");
+// ed.apply();
+// }
+// }
+// ed.putInt(PREF_VER_KEY, prefver);
+// ed.apply();
+ }
+
+ static int getAppVersion(Context ctx) {
+ int ver = -1;
+ try {
+ ver = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionCode;
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Could not find package name");
+ }
+ return ver;
+ }
+}
diff --git a/src/dk/network42/osmfocus/SparseMatrix.java b/src/dk/network42/osmfocus/SparseMatrix.java
new file mode 100644
index 0000000..f558383
--- /dev/null
+++ b/src/dk/network42/osmfocus/SparseMatrix.java
@@ -0,0 +1,32 @@
+package dk.network42.osmfocus;
+
+import android.util.SparseArray;
+
+public class SparseMatrix {
+ private SparseArray> rows = new SparseArray>();
+
+ public SparseMatrix() {
+ }
+
+ public void put(int x, int y, E elem) {
+ SparseArray a = rows.get(y);
+ if (a==null) {
+ a = new SparseArray();
+ rows.put(y, a);
+ }
+ a.put(x, elem);
+ }
+
+ public E get(int x, int y) {
+ SparseArray a = rows.get(y);
+ if (a==null)
+ return null;
+ return a.get(x);
+ }
+
+ public void delete(int x, int y) {
+ SparseArray a = rows.get(y);
+ if (a!=null)
+ a.delete(x);
+ }
+}