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); + } +}