From 4913abb61d5fb7c095d070a75f5ef2a8027d3b4c Mon Sep 17 00:00:00 2001 From: Francisco Dans Date: Wed, 20 Nov 2013 23:08:09 +0000 Subject: [PATCH] library + test --- .classpath | 36 + .idea/.name | 1 + .idea/compiler.xml | 23 + .idea/copyright/profiles_settings.xml | 5 + .idea/encodings.xml | 5 + .idea/libraries/slf4j_api_1_7_5.xml | 9 + .idea/misc.xml | 171 ++ .idea/modules.xml | 9 + .idea/scopes/scope_settings.xml | 5 + .idea/uiDesigner.xml | 125 ++ .idea/vcs.xml | 7 + .idea/workspace.xml | 310 +++ .project | 23 + OSMDroidTests | 1 + build.gradle | 4 + pom.xml | 48 + .../java/microsoft/mappoint/TileSystem.java | 265 +++ .../controller/MultiTouchController.java | 818 ++++++++ .../osmdroid/DefaultResourceProxyImpl.java | 175 ++ .../org/osmdroid/LocationListenerProxy.java | 63 + src/main/java/org/osmdroid/ResourceProxy.java | 63 + .../osmdroid/SensorEventListenerProxy.java | 44 + src/main/java/org/osmdroid/api/IGeoPoint.java | 11 + src/main/java/org/osmdroid/api/IMap.java | 107 + .../java/org/osmdroid/api/IMapController.java | 24 + src/main/java/org/osmdroid/api/IMapView.java | 26 + .../org/osmdroid/api/IMyLocationOverlay.java | 33 + src/main/java/org/osmdroid/api/IPosition.java | 37 + .../java/org/osmdroid/api/IProjection.java | 56 + src/main/java/org/osmdroid/api/Marker.java | 90 + .../osmdroid/api/OnCameraChangeListener.java | 10 + src/main/java/org/osmdroid/api/Polyline.java | 64 + .../org/osmdroid/contributor/OSMUploader.java | 276 +++ .../osmdroid/contributor/RouteRecorder.java | 61 + .../contributor/util/RecordedGeoPoint.java | 73 + .../util/RecordedRouteGPXFormatter.java | 131 ++ .../org/osmdroid/contributor/util/Util.java | 78 + .../OpenStreetMapContributorConstants.java | 30 + .../osmdroid/events/DelayedMapListener.java | 100 + .../java/org/osmdroid/events/MapAdapter.java | 21 + .../java/org/osmdroid/events/MapEvent.java | 10 + .../java/org/osmdroid/events/MapListener.java | 20 + .../java/org/osmdroid/events/ScrollEvent.java | 46 + .../java/org/osmdroid/events/ZoomEvent.java | 37 + .../org/osmdroid/http/HttpClientFactory.java | 39 + .../org/osmdroid/http/IHttpClientFactory.java | 16 + .../org/osmdroid/tileprovider/BitmapPool.java | 65 + .../tileprovider/ExpirableBitmapDrawable.java | 50 + .../IMapTileProviderCallback.java | 39 + .../tileprovider/IRegisterReceiver.java | 12 + .../tileprovider/LRUMapTileCache.java | 90 + .../org/osmdroid/tileprovider/MapTile.java | 66 + .../osmdroid/tileprovider/MapTileCache.java | 88 + .../tileprovider/MapTileProviderArray.java | 236 +++ .../tileprovider/MapTileProviderBase.java | 426 ++++ .../tileprovider/MapTileProviderBasic.java | 63 + .../tileprovider/MapTileRequestState.java | 45 + .../tileprovider/ReusableBitmapDrawable.java | 53 + .../OpenStreetMapTileProviderConstants.java | 72 + .../modules/ArchiveFileFactory.java | 56 + .../ConfigurablePriorityThreadFactory.java | 30 + .../modules/DatabaseFileArchive.java | 60 + .../tileprovider/modules/GEMFFileArchive.java | 34 + .../tileprovider/modules/IArchiveFile.java | 16 + .../modules/IFilesystemCache.java | 29 + .../modules/INetworkAvailablityCheck.java | 12 + .../modules/MBTilesFileArchive.java | 74 + .../modules/MapTileDownloader.java | 251 +++ .../modules/MapTileFileArchiveProvider.java | 234 +++ .../MapTileFileStorageProviderBase.java | 86 + .../modules/MapTileFilesystemProvider.java | 178 ++ .../modules/MapTileModuleProviderBase.java | 325 ++++ .../modules/NetworkAvailabliltyCheck.java | 50 + .../tileprovider/modules/TileWriter.java | 243 +++ .../tileprovider/modules/ZipFileArchive.java | 48 + .../tilesource/BitmapTileSourceBase.java | 158 ++ .../tilesource/CloudmadeTileSource.java | 60 + .../tilesource/IStyledTileSource.java | 15 + .../tileprovider/tilesource/ITileSource.java | 85 + .../tilesource/MapBoxTileSource.java | 95 + .../tilesource/OnlineTileSourceBase.java | 26 + .../tilesource/QuadTreeTileSource.java | 42 + .../tilesource/TileSourceFactory.java | 143 ++ .../tileprovider/tilesource/XYTileSource.java | 20 + .../tileprovider/util/CloudmadeUtil.java | 142 ++ .../tileprovider/util/ManifestUtil.java | 44 + .../util/SimpleInvalidationHandler.java | 25 + .../util/SimpleRegisterReceiver.java | 28 + .../tileprovider/util/StreamUtils.java | 91 + .../java/org/osmdroid/util/BoundingBoxE6.java | 270 +++ src/main/java/org/osmdroid/util/GEMFFile.java | 696 +++++++ src/main/java/org/osmdroid/util/GeoPoint.java | 330 ++++ .../java/org/osmdroid/util/GeometryMath.java | 63 + .../java/org/osmdroid/util/LocationUtils.java | 48 + src/main/java/org/osmdroid/util/MyMath.java | 63 + .../osmdroid/util/NetworkLocationIgnorer.java | 42 + src/main/java/org/osmdroid/util/Position.java | 57 + .../org/osmdroid/util/ResourceProxyImpl.java | 54 + .../java/org/osmdroid/util/TileLooper.java | 46 + .../java/org/osmdroid/util/TileSystem.java | 115 ++ .../osmdroid/util/constants/GeoConstants.java | 18 + .../util/constants/UtilConstants.java | 10 + .../org/osmdroid/views/MapController.java | 310 +++ .../org/osmdroid/views/MapControllerOld.java | 685 +++++++ src/main/java/org/osmdroid/views/MapView.java | 1721 +++++++++++++++++ .../overlay/DirectedLocationOverlay.java | 159 ++ .../views/overlay/IOverlayMenuProvider.java | 26 + .../views/overlay/ItemizedIconOverlay.java | 217 +++ .../views/overlay/ItemizedOverlay.java | 350 ++++ .../overlay/ItemizedOverlayControlView.java | 148 ++ .../overlay/ItemizedOverlayWithFocus.java | 277 +++ .../views/overlay/MinimapOverlay.java | 310 +++ .../views/overlay/MyLocationOverlay.java | 881 +++++++++ .../org/osmdroid/views/overlay/Overlay.java | 314 +++ .../osmdroid/views/overlay/OverlayItem.java | 167 ++ .../views/overlay/OverlayManager.java | 366 ++++ .../osmdroid/views/overlay/PathOverlay.java | 261 +++ .../views/overlay/SafeDrawOverlay.java | 99 + .../views/overlay/ScaleBarOverlay.java | 583 ++++++ .../views/overlay/SimpleLocationOverlay.java | 87 + .../osmdroid/views/overlay/TilesOverlay.java | 374 ++++ .../views/overlay/compass/CompassOverlay.java | 448 +++++ .../overlay/compass/IOrientationConsumer.java | 7 + .../overlay/compass/IOrientationProvider.java | 11 + .../InternalCompassOrientationProvider.java | 73 + .../mylocation/GpsMyLocationProvider.java | 120 ++ .../mylocation/IMyLocationConsumer.java | 7 + .../mylocation/IMyLocationProvider.java | 11 + .../mylocation/MyLocationNewOverlay.java | 549 ++++++ .../views/safecanvas/ISafeCanvas.java | 1079 +++++++++++ .../views/safecanvas/SafeBitmapShader.java | 42 + .../views/safecanvas/SafeDashPathEffect.java | 43 + .../osmdroid/views/safecanvas/SafePaint.java | 25 + .../safecanvas/SafeTranslatedCanvas.java | 580 ++++++ .../views/safecanvas/SafeTranslatedPath.java | 466 +++++ .../org/osmdroid/views/util/Mercator.java | 159 ++ .../java/org/osmdroid/views/util/MyMath.java | 73 + .../osmdroid/views/util/PathProjection.java | 96 + .../util/constants/MapViewConstants.java | 36 + .../views/util/constants/MathConstants.java | 24 + .../util/constants/OverlayConstants.java | 16 + src/main/java/slf4j-api-1.7.5.jar | Bin 0 -> 26084 bytes src/main/main.iml | 14 + src/main/resources/org/osmdroid/center.png | Bin 0 -> 2335 bytes .../org/osmdroid/direction_arrow.png | Bin 0 -> 2642 bytes .../org/osmdroid/ic_menu_compass.png | Bin 0 -> 3943 bytes .../org/osmdroid/ic_menu_mapmode.png | Bin 0 -> 1923 bytes .../org/osmdroid/ic_menu_mylocation.png | Bin 0 -> 5307 bytes .../org/osmdroid/ic_menu_offline.png | Bin 0 -> 4733 bytes .../resources/org/osmdroid/marker_default.png | Bin 0 -> 1912 bytes .../osmdroid/marker_default_focused_base.png | Bin 0 -> 664 bytes .../resources/org/osmdroid/navto_small.png | Bin 0 -> 1914 bytes src/main/resources/org/osmdroid/next.png | Bin 0 -> 1670 bytes src/main/resources/org/osmdroid/person.png | Bin 0 -> 1531 bytes src/main/resources/org/osmdroid/previous.png | Bin 0 -> 1648 bytes .../osmdroid/DefaultResourceProxyTest.java | 32 + .../java/org/osmdroid/util/GeoPointTest.java | 124 ++ src/test/test.iml | 12 + 158 files changed, 20805 insertions(+) create mode 100644 .classpath create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/libraries/slf4j_api_1_7_5.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 .project create mode 160000 OSMDroidTests create mode 100644 build.gradle create mode 100644 pom.xml create mode 100644 src/main/java/microsoft/mappoint/TileSystem.java create mode 100644 src/main/java/org/metalev/multitouch/controller/MultiTouchController.java create mode 100644 src/main/java/org/osmdroid/DefaultResourceProxyImpl.java create mode 100644 src/main/java/org/osmdroid/LocationListenerProxy.java create mode 100644 src/main/java/org/osmdroid/ResourceProxy.java create mode 100644 src/main/java/org/osmdroid/SensorEventListenerProxy.java create mode 100644 src/main/java/org/osmdroid/api/IGeoPoint.java create mode 100644 src/main/java/org/osmdroid/api/IMap.java create mode 100644 src/main/java/org/osmdroid/api/IMapController.java create mode 100644 src/main/java/org/osmdroid/api/IMapView.java create mode 100644 src/main/java/org/osmdroid/api/IMyLocationOverlay.java create mode 100644 src/main/java/org/osmdroid/api/IPosition.java create mode 100644 src/main/java/org/osmdroid/api/IProjection.java create mode 100644 src/main/java/org/osmdroid/api/Marker.java create mode 100644 src/main/java/org/osmdroid/api/OnCameraChangeListener.java create mode 100644 src/main/java/org/osmdroid/api/Polyline.java create mode 100644 src/main/java/org/osmdroid/contributor/OSMUploader.java create mode 100644 src/main/java/org/osmdroid/contributor/RouteRecorder.java create mode 100644 src/main/java/org/osmdroid/contributor/util/RecordedGeoPoint.java create mode 100644 src/main/java/org/osmdroid/contributor/util/RecordedRouteGPXFormatter.java create mode 100644 src/main/java/org/osmdroid/contributor/util/Util.java create mode 100644 src/main/java/org/osmdroid/contributor/util/constants/OpenStreetMapContributorConstants.java create mode 100644 src/main/java/org/osmdroid/events/DelayedMapListener.java create mode 100644 src/main/java/org/osmdroid/events/MapAdapter.java create mode 100644 src/main/java/org/osmdroid/events/MapEvent.java create mode 100644 src/main/java/org/osmdroid/events/MapListener.java create mode 100644 src/main/java/org/osmdroid/events/ScrollEvent.java create mode 100644 src/main/java/org/osmdroid/events/ZoomEvent.java create mode 100644 src/main/java/org/osmdroid/http/HttpClientFactory.java create mode 100644 src/main/java/org/osmdroid/http/IHttpClientFactory.java create mode 100644 src/main/java/org/osmdroid/tileprovider/BitmapPool.java create mode 100644 src/main/java/org/osmdroid/tileprovider/ExpirableBitmapDrawable.java create mode 100644 src/main/java/org/osmdroid/tileprovider/IMapTileProviderCallback.java create mode 100644 src/main/java/org/osmdroid/tileprovider/IRegisterReceiver.java create mode 100644 src/main/java/org/osmdroid/tileprovider/LRUMapTileCache.java create mode 100644 src/main/java/org/osmdroid/tileprovider/MapTile.java create mode 100644 src/main/java/org/osmdroid/tileprovider/MapTileCache.java create mode 100644 src/main/java/org/osmdroid/tileprovider/MapTileProviderArray.java create mode 100644 src/main/java/org/osmdroid/tileprovider/MapTileProviderBase.java create mode 100644 src/main/java/org/osmdroid/tileprovider/MapTileProviderBasic.java create mode 100644 src/main/java/org/osmdroid/tileprovider/MapTileRequestState.java create mode 100644 src/main/java/org/osmdroid/tileprovider/ReusableBitmapDrawable.java create mode 100644 src/main/java/org/osmdroid/tileprovider/constants/OpenStreetMapTileProviderConstants.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/ArchiveFileFactory.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/ConfigurablePriorityThreadFactory.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/DatabaseFileArchive.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/GEMFFileArchive.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/IArchiveFile.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/IFilesystemCache.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/INetworkAvailablityCheck.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/MBTilesFileArchive.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/MapTileDownloader.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/MapTileFileArchiveProvider.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/MapTileFileStorageProviderBase.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/MapTileFilesystemProvider.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/MapTileModuleProviderBase.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/NetworkAvailabliltyCheck.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/TileWriter.java create mode 100644 src/main/java/org/osmdroid/tileprovider/modules/ZipFileArchive.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/BitmapTileSourceBase.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/CloudmadeTileSource.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/IStyledTileSource.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/ITileSource.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/MapBoxTileSource.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/OnlineTileSourceBase.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/QuadTreeTileSource.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/TileSourceFactory.java create mode 100644 src/main/java/org/osmdroid/tileprovider/tilesource/XYTileSource.java create mode 100644 src/main/java/org/osmdroid/tileprovider/util/CloudmadeUtil.java create mode 100644 src/main/java/org/osmdroid/tileprovider/util/ManifestUtil.java create mode 100644 src/main/java/org/osmdroid/tileprovider/util/SimpleInvalidationHandler.java create mode 100644 src/main/java/org/osmdroid/tileprovider/util/SimpleRegisterReceiver.java create mode 100644 src/main/java/org/osmdroid/tileprovider/util/StreamUtils.java create mode 100644 src/main/java/org/osmdroid/util/BoundingBoxE6.java create mode 100644 src/main/java/org/osmdroid/util/GEMFFile.java create mode 100644 src/main/java/org/osmdroid/util/GeoPoint.java create mode 100644 src/main/java/org/osmdroid/util/GeometryMath.java create mode 100644 src/main/java/org/osmdroid/util/LocationUtils.java create mode 100644 src/main/java/org/osmdroid/util/MyMath.java create mode 100644 src/main/java/org/osmdroid/util/NetworkLocationIgnorer.java create mode 100644 src/main/java/org/osmdroid/util/Position.java create mode 100644 src/main/java/org/osmdroid/util/ResourceProxyImpl.java create mode 100644 src/main/java/org/osmdroid/util/TileLooper.java create mode 100644 src/main/java/org/osmdroid/util/TileSystem.java create mode 100644 src/main/java/org/osmdroid/util/constants/GeoConstants.java create mode 100644 src/main/java/org/osmdroid/util/constants/UtilConstants.java create mode 100644 src/main/java/org/osmdroid/views/MapController.java create mode 100644 src/main/java/org/osmdroid/views/MapControllerOld.java create mode 100644 src/main/java/org/osmdroid/views/MapView.java create mode 100644 src/main/java/org/osmdroid/views/overlay/DirectedLocationOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/IOverlayMenuProvider.java create mode 100644 src/main/java/org/osmdroid/views/overlay/ItemizedIconOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/ItemizedOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/ItemizedOverlayControlView.java create mode 100644 src/main/java/org/osmdroid/views/overlay/ItemizedOverlayWithFocus.java create mode 100644 src/main/java/org/osmdroid/views/overlay/MinimapOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/MyLocationOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/Overlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/OverlayItem.java create mode 100644 src/main/java/org/osmdroid/views/overlay/OverlayManager.java create mode 100644 src/main/java/org/osmdroid/views/overlay/PathOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/SafeDrawOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/ScaleBarOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/SimpleLocationOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/TilesOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/compass/CompassOverlay.java create mode 100644 src/main/java/org/osmdroid/views/overlay/compass/IOrientationConsumer.java create mode 100644 src/main/java/org/osmdroid/views/overlay/compass/IOrientationProvider.java create mode 100644 src/main/java/org/osmdroid/views/overlay/compass/InternalCompassOrientationProvider.java create mode 100644 src/main/java/org/osmdroid/views/overlay/mylocation/GpsMyLocationProvider.java create mode 100644 src/main/java/org/osmdroid/views/overlay/mylocation/IMyLocationConsumer.java create mode 100644 src/main/java/org/osmdroid/views/overlay/mylocation/IMyLocationProvider.java create mode 100644 src/main/java/org/osmdroid/views/overlay/mylocation/MyLocationNewOverlay.java create mode 100644 src/main/java/org/osmdroid/views/safecanvas/ISafeCanvas.java create mode 100644 src/main/java/org/osmdroid/views/safecanvas/SafeBitmapShader.java create mode 100644 src/main/java/org/osmdroid/views/safecanvas/SafeDashPathEffect.java create mode 100644 src/main/java/org/osmdroid/views/safecanvas/SafePaint.java create mode 100644 src/main/java/org/osmdroid/views/safecanvas/SafeTranslatedCanvas.java create mode 100644 src/main/java/org/osmdroid/views/safecanvas/SafeTranslatedPath.java create mode 100644 src/main/java/org/osmdroid/views/util/Mercator.java create mode 100644 src/main/java/org/osmdroid/views/util/MyMath.java create mode 100644 src/main/java/org/osmdroid/views/util/PathProjection.java create mode 100644 src/main/java/org/osmdroid/views/util/constants/MapViewConstants.java create mode 100644 src/main/java/org/osmdroid/views/util/constants/MathConstants.java create mode 100644 src/main/java/org/osmdroid/views/util/constants/OverlayConstants.java create mode 100644 src/main/java/slf4j-api-1.7.5.jar create mode 100644 src/main/main.iml create mode 100644 src/main/resources/org/osmdroid/center.png create mode 100644 src/main/resources/org/osmdroid/direction_arrow.png create mode 100644 src/main/resources/org/osmdroid/ic_menu_compass.png create mode 100644 src/main/resources/org/osmdroid/ic_menu_mapmode.png create mode 100644 src/main/resources/org/osmdroid/ic_menu_mylocation.png create mode 100644 src/main/resources/org/osmdroid/ic_menu_offline.png create mode 100644 src/main/resources/org/osmdroid/marker_default.png create mode 100644 src/main/resources/org/osmdroid/marker_default_focused_base.png create mode 100644 src/main/resources/org/osmdroid/navto_small.png create mode 100644 src/main/resources/org/osmdroid/next.png create mode 100644 src/main/resources/org/osmdroid/person.png create mode 100644 src/main/resources/org/osmdroid/previous.png create mode 100644 src/test/java/org/osmdroid/DefaultResourceProxyTest.java create mode 100644 src/test/java/org/osmdroid/util/GeoPointTest.java create mode 100644 src/test/test.iml diff --git a/.classpath b/.classpath new file mode 100644 index 000000000..534b5e52f --- /dev/null +++ b/.classpath @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000..f3d8b4cca --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +osmdroid-android \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 000000000..217af471a --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 000000000..3572571ad --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..e206d70d8 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/libraries/slf4j_api_1_7_5.xml b/.idea/libraries/slf4j_api_1_7_5.xml new file mode 100644 index 000000000..c33cc8576 --- /dev/null +++ b/.idea/libraries/slf4j_api_1_7_5.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..23ed0510d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + 5050 + + + + + + + 1.6 + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..4585223fb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 000000000..922003b84 --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 000000000..3b0002030 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..def6a6a18 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 000000000..ae052d8ef --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + 5050 + + + + + + + 1384554246032 + 1384554246032 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 000000000..26c159607 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + osmdroid-android + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/OSMDroidTests b/OSMDroidTests new file mode 160000 index 000000000..730f60418 --- /dev/null +++ b/OSMDroidTests @@ -0,0 +1 @@ +Subproject commit 730f60418dde230f688add7c24c9ebf742ef5680 diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..26be5a1a8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,4 @@ +apply plugin: 'java' +sourceSets { + main.java.srcDirs = ['src\main\java','src\main\resources','src\test\java','src\test\resources'] +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..0ada2d893 --- /dev/null +++ b/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + + org.osmdroid + osmdroid-parent + 4.1-SNAPSHOT + + + osmdroid-android + jar + + OSMdroid Android + An Android library to display OpenStreetMap views. + + + + + com.google.android + android + + + org.apache.httpcomponents + httpmime + + + org.apache.james + apache-mime4j + + + + + org.slf4j + slf4j-api + + + + + org.slf4j + slf4j-log4j12 + + + junit + junit + + + + diff --git a/src/main/java/microsoft/mappoint/TileSystem.java b/src/main/java/microsoft/mappoint/TileSystem.java new file mode 100644 index 000000000..79f894a38 --- /dev/null +++ b/src/main/java/microsoft/mappoint/TileSystem.java @@ -0,0 +1,265 @@ +package microsoft.mappoint; + +/* + * http://msdn.microsoft.com/en-us/library/bb259689.aspx + * + * Copyright (c) 2006-2009 Microsoft Corporation. All rights reserved. + * + * + */ + +import org.osmdroid.util.GeoPoint; + +import android.graphics.Point; + +/** + * This class provides methods to handle the Mercator projection that is used for the osmdroid tile + * system. + */ +public final class TileSystem { + + protected static int mTileSize = 256; + private static final double EarthRadius = 6378137; + private static final double MinLatitude = -85.05112878; + private static final double MaxLatitude = 85.05112878; + private static final double MinLongitude = -180; + private static final double MaxLongitude = 180; + + public static void setTileSize(final int tileSize) { + mTileSize = tileSize; + } + + public static int getTileSize() { + return mTileSize; + } + + /** + * Clips a number to the specified minimum and maximum values. + * + * @param n + * The number to clip + * @param minValue + * Minimum allowable value + * @param maxValue + * Maximum allowable value + * @return The clipped value. + */ + private static double Clip(final double n, final double minValue, final double maxValue) { + return Math.min(Math.max(n, minValue), maxValue); + } + + /** + * Determines the map width and height (in pixels) at a specified level of detail. + * + * @param levelOfDetail + * Level of detail, from 1 (lowest detail) to 23 (highest detail) + * @return The map width and height in pixels + */ + + public static int MapSize(final int levelOfDetail) { + return mTileSize << levelOfDetail; + } + + /** + * Determines the ground resolution (in meters per pixel) at a specified latitude and level of + * detail. + * + * @param latitude + * Latitude (in degrees) at which to measure the ground resolution + * @param levelOfDetail + * Level of detail, from 1 (lowest detail) to 23 (highest detail) + * @return The ground resolution, in meters per pixel + */ + public static double GroundResolution(double latitude, final int levelOfDetail) { + latitude = Clip(latitude, MinLatitude, MaxLatitude); + return Math.cos(latitude * Math.PI / 180) * 2 * Math.PI * EarthRadius + / MapSize(levelOfDetail); + } + + /** + * Determines the map scale at a specified latitude, level of detail, and screen resolution. + * + * @param latitude + * Latitude (in degrees) at which to measure the map scale + * @param levelOfDetail + * Level of detail, from 1 (lowest detail) to 23 (highest detail) + * @param screenDpi + * Resolution of the screen, in dots per inch + * @return The map scale, expressed as the denominator N of the ratio 1 : N + */ + public static double MapScale(final double latitude, final int levelOfDetail, + final int screenDpi) { + return GroundResolution(latitude, levelOfDetail) * screenDpi / 0.0254; + } + + /** + * Converts a point from latitude/longitude WGS-84 coordinates (in degrees) into pixel XY + * coordinates at a specified level of detail. + * + * @param latitude + * Latitude of the point, in degrees + * @param longitude + * Longitude of the point, in degrees + * @param levelOfDetail + * Level of detail, from 1 (lowest detail) to 23 (highest detail) + * @param reuse + * An optional Point to be recycled, or null to create a new one automatically + * @return Output parameter receiving the X and Y coordinates in pixels + */ + public static Point LatLongToPixelXY(double latitude, double longitude, + final int levelOfDetail, final Point reuse) { + final Point out = (reuse == null ? new Point() : reuse); + + latitude = Clip(latitude, MinLatitude, MaxLatitude); + longitude = Clip(longitude, MinLongitude, MaxLongitude); + + final double x = (longitude + 180) / 360; + final double sinLatitude = Math.sin(latitude * Math.PI / 180); + final double y = 0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI); + + final int mapSize = MapSize(levelOfDetail); + out.x = (int) Clip(x * mapSize + 0.5, 0, mapSize - 1); + out.y = (int) Clip(y * mapSize + 0.5, 0, mapSize - 1); + return out; + } + + /** + * Converts a pixel from pixel XY coordinates at a specified level of detail into + * latitude/longitude WGS-84 coordinates (in degrees). + * + * @param pixelX + * X coordinate of the point, in pixels + * @param pixelY + * Y coordinate of the point, in pixels + * @param levelOfDetail + * Level of detail, from 1 (lowest detail) to 23 (highest detail) + * @param reuse + * An optional GeoPoint to be recycled, or null to create a new one automatically + * @return Output parameter receiving the latitude and longitude in degrees. + */ + public static GeoPoint PixelXYToLatLong(final int pixelX, final int pixelY, + final int levelOfDetail, final GeoPoint reuse) { + final GeoPoint out = (reuse == null ? new GeoPoint(0, 0) : reuse); + + final double mapSize = MapSize(levelOfDetail); + final double x = (Clip(pixelX, 0, mapSize - 1) / mapSize) - 0.5; + final double y = 0.5 - (Clip(pixelY, 0, mapSize - 1) / mapSize); + + final double latitude = 90 - 360 * Math.atan(Math.exp(-y * 2 * Math.PI)) / Math.PI; + final double longitude = 360 * x; + + out.setLatitudeE6((int) (latitude * 1E6)); + out.setLongitudeE6((int) (longitude * 1E6)); + return out; + } + + /** + * Converts pixel XY coordinates into tile XY coordinates of the tile containing the specified + * pixel. + * + * @param pixelX + * Pixel X coordinate + * @param pixelY + * Pixel Y coordinate + * @param reuse + * An optional Point to be recycled, or null to create a new one automatically + * @return Output parameter receiving the tile X and Y coordinates + */ + public static Point PixelXYToTileXY(final int pixelX, final int pixelY, final Point reuse) { + final Point out = (reuse == null ? new Point() : reuse); + + out.x = pixelX / mTileSize; + out.y = pixelY / mTileSize; + return out; + } + + /** + * Converts tile XY coordinates into pixel XY coordinates of the upper-left pixel of the + * specified tile. + * + * @param tileX + * Tile X coordinate + * @param tileY + * Tile X coordinate + * @param reuse + * An optional Point to be recycled, or null to create a new one automatically + * @return Output parameter receiving the pixel X and Y coordinates + */ + public static Point TileXYToPixelXY(final int tileX, final int tileY, final Point reuse) { + final Point out = (reuse == null ? new Point() : reuse); + + out.x = tileX * mTileSize; + out.y = tileY * mTileSize; + return out; + } + + /** + * Converts tile XY coordinates into a QuadKey at a specified level of detail. + * + * @param tileX + * Tile X coordinate + * @param tileY + * Tile Y coordinate + * @param levelOfDetail + * Level of detail, from 1 (lowest detail) to 23 (highest detail) + * @return A string containing the QuadKey + */ + public static String TileXYToQuadKey(final int tileX, final int tileY, final int levelOfDetail) { + final StringBuilder quadKey = new StringBuilder(); + for (int i = levelOfDetail; i > 0; i--) { + char digit = '0'; + final int mask = 1 << (i - 1); + if ((tileX & mask) != 0) { + digit++; + } + if ((tileY & mask) != 0) { + digit++; + digit++; + } + quadKey.append(digit); + } + return quadKey.toString(); + } + + /** + * Converts a QuadKey into tile XY coordinates. + * + * @param quadKey + * QuadKey of the tile + * @param reuse + * An optional Point to be recycled, or null to create a new one automatically + * @return Output parameter receiving the tile X and y coordinates + */ + public static Point QuadKeyToTileXY(final String quadKey, final Point reuse) { + final Point out = (reuse == null ? new Point() : reuse); + int tileX = 0; + int tileY = 0; + + final int levelOfDetail = quadKey.length(); + for (int i = levelOfDetail; i > 0; i--) { + final int mask = 1 << (i - 1); + switch (quadKey.charAt(levelOfDetail - i)) { + case '0': + break; + + case '1': + tileX |= mask; + break; + + case '2': + tileY |= mask; + break; + + case '3': + tileX |= mask; + tileY |= mask; + break; + + default: + throw new IllegalArgumentException("Invalid QuadKey digit sequence."); + } + } + out.set(tileX, tileY); + return out; + } +} diff --git a/src/main/java/org/metalev/multitouch/controller/MultiTouchController.java b/src/main/java/org/metalev/multitouch/controller/MultiTouchController.java new file mode 100644 index 000000000..be9404953 --- /dev/null +++ b/src/main/java/org/metalev/multitouch/controller/MultiTouchController.java @@ -0,0 +1,818 @@ +package org.metalev.multitouch.controller; + +/** + * MultiTouchController.java + * + * Author: Luke Hutchison (luke.hutch@mit.edu) + * Please drop me an email if you use this code so I can list your project here! + * + * Usage: + * + * public class MyMTView extends View implements MultiTouchObjectCanvas { + * + * private MultiTouchController multiTouchController = new MultiTouchController(this); + * + * // Pass touch events to the MT controller + * public boolean onTouchEvent(MotionEvent event) { + * return multiTouchController.onTouchEvent(event); + * } + * + * // ... then implement the MultiTouchObjectCanvas interface here, see details in the comments of that interface. + * } + * + * + * Changelog: + * 2010-06-09 v1.5.1 Some API changes to make it possible to selectively update or not update scale / rotation. + * Fixed anisotropic zoom. Cleaned up rotation code. Added more comments. Better var names. (LH) + * 2010-06-09 v1.4 Added ability to track pinch rotation (Mickael Despesse, author of "Face Frenzy") and anisotropic pinch-zoom (LH) + * 2010-06-09 v1.3.3 Bugfixes for Android-2.1; added optional debug info (LH) + * 2010-06-09 v1.3 Ported to Android-2.2 (handle ACTION_POINTER_* actions); fixed several bugs; refactoring; documentation (LH) + * 2010-05-17 v1.2.1 Dual-licensed under Apache and GPL licenses + * 2010-02-18 v1.2 Support for compilation under Android 1.5/1.6 using introspection (mmin, author of handyCalc) + * 2010-01-08 v1.1.1 Bugfixes to Cyanogen's patch that only showed up in more complex uses of controller (LH) + * 2010-01-06 v1.1 Modified for official level 5 MT API (Cyanogen) + * 2009-01-25 v1.0 Original MT controller, released for hacked G1 kernel (LH) + * + * Planned features: + * - Add inertia (flick-pinch-zoom or flick-scroll) + * + * Known usages: + * - Mickael Despesse's "Face Frenzy" face distortion app, to be published to the Market soon + * - Yuan Chin's fork of ADW Launcher to support multitouch + * - David Byrne's fractal viewing app Fractoid + * - mmin's handyCalc calculator + * - My own "MultiTouch Visualizer 2" in the Market + * - Formerly: The browser in cyanogenmod (and before that, JesusFreke), and other firmwares like dwang5. This usage has been + * replaced with official pinch/zoom in Maps, Browser and Gallery[3D] as of API level 5. + * + * License: + * Dual-licensed under the Apache License v2 and the GPL v2. + */ + +import java.lang.reflect.Method; + +import android.util.Log; +import android.view.MotionEvent; + +/** + * A class that simplifies the implementation of multitouch in applications. Subclass this and read the fields here as needed in subclasses. + * + * @author Luke Hutchison + */ +public class MultiTouchController { + + /** + * Time in ms required after a change in event status (e.g. putting down or lifting off the second finger) before events actually do anything -- + * helps eliminate noisy jumps that happen on change of status + */ + private static final long EVENT_SETTLE_TIME_INTERVAL = 20; + + /** + * The biggest possible abs val of the change in x or y between multitouch events (larger dx/dy events are ignored) -- helps eliminate jumps in + * pointer position on finger 2 up/down. + */ + private static final float MAX_MULTITOUCH_POS_JUMP_SIZE = 30.0f; + + /** + * The biggest possible abs val of the change in multitouchWidth or multitouchHeight between multitouch events (larger-jump events are ignored) -- + * helps eliminate jumps in pointer position on finger 2 up/down. + */ + private static final float MAX_MULTITOUCH_DIM_JUMP_SIZE = 40.0f; + + /** The smallest possible distance between multitouch points (used to avoid div-by-zero errors and display glitches) */ + private static final float MIN_MULTITOUCH_SEPARATION = 30.0f; + + /** The max number of touch points that can be present on the screen at once */ + public static final int MAX_TOUCH_POINTS = 20; + + /** Generate tons of log entries for debugging */ + public static final boolean DEBUG = false; + + // ---------------------------------------------------------------------------------------------------------------------- + + MultiTouchObjectCanvas objectCanvas; + + /** The current touch point */ + private PointInfo mCurrPt; + + /** The previous touch point */ + private PointInfo mPrevPt; + + /** Fields extracted from mCurrPt */ + private float mCurrPtX, mCurrPtY, mCurrPtDiam, mCurrPtWidth, mCurrPtHeight, mCurrPtAng; + + /** + * Extract fields from mCurrPt, respecting the update* fields of mCurrPt. This just avoids code duplication. I hate that Java doesn't support + * higher-order functions, tuples or multiple return values from functions. + */ + private void extractCurrPtInfo() { + // Get new drag/pinch params. Only read multitouch fields that are needed, + // to avoid unnecessary computation (diameter and angle are expensive operations). + mCurrPtX = mCurrPt.getX(); + mCurrPtY = mCurrPt.getY(); + mCurrPtDiam = Math.max(MIN_MULTITOUCH_SEPARATION * .71f, !mCurrXform.updateScale ? 0.0f : mCurrPt.getMultiTouchDiameter()); + mCurrPtWidth = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchWidth()); + mCurrPtHeight = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchHeight()); + mCurrPtAng = !mCurrXform.updateAngle ? 0.0f : mCurrPt.getMultiTouchAngle(); + } + + // ---------------------------------------------------------------------------------------------------------------------- + + /** Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses */ + private boolean handleSingleTouchEvents; + + /** The object being dragged/stretched */ + private T selectedObject = null; + + /** Current position and scale of the dragged object */ + private PositionAndScale mCurrXform = new PositionAndScale(); + + /** Drag/pinch start time and time to ignore spurious events until (to smooth over event noise) */ + private long mSettleStartTime, mSettleEndTime; + + /** Conversion from object coords to screen coords */ + private float startPosX, startPosY; + + /** Conversion between scale and width, and object angle and start pinch angle */ + private float startScaleOverPinchDiam, startAngleMinusPinchAngle; + + /** Conversion between X scale and width, and Y scale and height */ + private float startScaleXOverPinchWidth, startScaleYOverPinchHeight; + + // ---------------------------------------------------------------------------------------------------------------------- + + /** No touch points down. */ + private static final int MODE_NOTHING = 0; + + /** One touch point down, dragging an object. */ + private static final int MODE_DRAG = 1; + + /** Two or more touch points down, stretching/rotating an object using the first two touch points. */ + private static final int MODE_PINCH = 2; + + /** Current drag mode */ + private int mMode = MODE_NOTHING; + + // ---------------------------------------------------------------------------------------------------------------------- + + /** Constructor that sets handleSingleTouchEvents to true */ + public MultiTouchController(MultiTouchObjectCanvas objectCanvas) { + this(objectCanvas, true); + } + + /** Full constructor */ + public MultiTouchController(MultiTouchObjectCanvas objectCanvas, boolean handleSingleTouchEvents) { + this.mCurrPt = new PointInfo(); + this.mPrevPt = new PointInfo(); + this.handleSingleTouchEvents = handleSingleTouchEvents; + this.objectCanvas = objectCanvas; + } + + // ------------------------------------------------------------------------------------ + + /** + * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses. Default: true + */ + protected void setHandleSingleTouchEvents(boolean handleSingleTouchEvents) { + this.handleSingleTouchEvents = handleSingleTouchEvents; + } + + /** + * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses. Default: true + */ + protected boolean getHandleSingleTouchEvents() { + return handleSingleTouchEvents; + } + + // ------------------------------------------------------------------------------------ + + public static final boolean multiTouchSupported; + private static Method m_getPointerCount; + private static Method m_getPointerId; + private static Method m_getPressure; + private static Method m_getHistoricalX; + private static Method m_getHistoricalY; + private static Method m_getHistoricalPressure; + private static Method m_getX; + private static Method m_getY; + private static int ACTION_POINTER_UP = 6; + private static int ACTION_POINTER_INDEX_SHIFT = 8; + + static { + boolean succeeded = false; + try { + // Android 2.0.1 stuff: + m_getPointerCount = MotionEvent.class.getMethod("getPointerCount"); + m_getPointerId = MotionEvent.class.getMethod("getPointerId", Integer.TYPE); + m_getPressure = MotionEvent.class.getMethod("getPressure", Integer.TYPE); + m_getHistoricalX = MotionEvent.class.getMethod("getHistoricalX", Integer.TYPE, Integer.TYPE); + m_getHistoricalY = MotionEvent.class.getMethod("getHistoricalY", Integer.TYPE, Integer.TYPE); + m_getHistoricalPressure = MotionEvent.class.getMethod("getHistoricalPressure", Integer.TYPE, Integer.TYPE); + m_getX = MotionEvent.class.getMethod("getX", Integer.TYPE); + m_getY = MotionEvent.class.getMethod("getY", Integer.TYPE); + succeeded = true; + } catch (Exception e) { + Log.e("MultiTouchController", "static initializer failed", e); + } + multiTouchSupported = succeeded; + if (multiTouchSupported) { + // Android 2.2+ stuff (the original Android 2.2 consts are declared above, + // and these actions aren't used previous to Android 2.2): + try { + ACTION_POINTER_UP = MotionEvent.class.getField("ACTION_POINTER_UP").getInt(null); + ACTION_POINTER_INDEX_SHIFT = MotionEvent.class.getField("ACTION_POINTER_INDEX_SHIFT").getInt(null); + } catch (Exception e) { + } + } + } + + // ------------------------------------------------------------------------------------ + + private static final float[] xVals = new float[MAX_TOUCH_POINTS]; + private static final float[] yVals = new float[MAX_TOUCH_POINTS]; + private static final float[] pressureVals = new float[MAX_TOUCH_POINTS]; + private static final int[] pointerIds = new int[MAX_TOUCH_POINTS]; + + /** Process incoming touch events */ + public boolean onTouchEvent(MotionEvent event) { + try { + int pointerCount = multiTouchSupported ? (Integer) m_getPointerCount.invoke(event) : 1; + if (DEBUG) + Log.i("MultiTouch", "Got here 1 - " + multiTouchSupported + " " + mMode + " " + handleSingleTouchEvents + " " + pointerCount); + if (mMode == MODE_NOTHING && !handleSingleTouchEvents && pointerCount == 1) + // Not handling initial single touch events, just pass them on + return false; + if (DEBUG) + Log.i("MultiTouch", "Got here 2"); + + // Handle history first (we sometimes get history with ACTION_MOVE events) + int action = event.getAction(); + int histLen = event.getHistorySize() / pointerCount; + for (int histIdx = 0; histIdx <= histLen; histIdx++) { + // Read from history entries until histIdx == histLen, then read from current event + boolean processingHist = histIdx < histLen; + if (!multiTouchSupported || pointerCount == 1) { + // Use single-pointer methods -- these are needed as a special case (for some weird reason) even if + // multitouch is supported but there's only one touch point down currently -- event.getX(0) etc. throw + // an exception if there's only one point down. + if (DEBUG) + Log.i("MultiTouch", "Got here 3"); + xVals[0] = processingHist ? event.getHistoricalX(histIdx) : event.getX(); + yVals[0] = processingHist ? event.getHistoricalY(histIdx) : event.getY(); + pressureVals[0] = processingHist ? event.getHistoricalPressure(histIdx) : event.getPressure(); + } else { + // Read x, y and pressure of each pointer + if (DEBUG) + Log.i("MultiTouch", "Got here 4"); + int numPointers = Math.min(pointerCount, MAX_TOUCH_POINTS); + if (DEBUG && pointerCount > MAX_TOUCH_POINTS) + Log.i("MultiTouch", "Got more pointers than MAX_TOUCH_POINTS"); + for (int ptrIdx = 0; ptrIdx < numPointers; ptrIdx++) { + int ptrId = (Integer) m_getPointerId.invoke(event, ptrIdx); + pointerIds[ptrIdx] = ptrId; + // N.B. if pointerCount == 1, then the following methods throw an array index out of range exception, + // and the code above is therefore required not just for Android 1.5/1.6 but also for when there is + // only one touch point on the screen -- pointlessly inconsistent :( + xVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalX.invoke(event, ptrIdx, histIdx) : m_getX.invoke(event, ptrIdx)); + yVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalY.invoke(event, ptrIdx, histIdx) : m_getY.invoke(event, ptrIdx)); + pressureVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalPressure.invoke(event, ptrIdx, histIdx) : m_getPressure + .invoke(event, ptrIdx)); + } + } + // Decode event + decodeTouchEvent(pointerCount, xVals, yVals, pressureVals, pointerIds, // + /* action = */processingHist ? MotionEvent.ACTION_MOVE : action, // + /* down = */processingHist ? true : action != MotionEvent.ACTION_UP // + && (action & ((1 << ACTION_POINTER_INDEX_SHIFT) - 1)) != ACTION_POINTER_UP // + && action != MotionEvent.ACTION_CANCEL, // + processingHist ? event.getHistoricalEventTime(histIdx) : event.getEventTime()); + } + + return true; + } catch (Exception e) { + // In case any of the introspection stuff fails (it shouldn't) + Log.e("MultiTouchController", "onTouchEvent() failed", e); + return false; + } + } + + private void decodeTouchEvent(int pointerCount, float[] x, float[] y, float[] pressure, int[] pointerIds, int action, boolean down, long eventTime) { + if (DEBUG) + Log.i("MultiTouch", "Got here 5 - " + pointerCount + " " + action + " " + down); + + // Swap curr/prev points + PointInfo tmp = mPrevPt; + mPrevPt = mCurrPt; + mCurrPt = tmp; + // Overwrite old prev point + mCurrPt.set(pointerCount, x, y, pressure, pointerIds, action, down, eventTime); + multiTouchController(); + } + + // ------------------------------------------------------------------------------------ + + /** Start dragging/pinching, or reset drag/pinch to current point if something goes out of range */ + private void anchorAtThisPositionAndScale() { + if (selectedObject == null) + return; + + // Get selected object's current position and scale + objectCanvas.getPositionAndScale(selectedObject, mCurrXform); + + // Figure out the object coords of the drag start point's screen coords. + // All stretching should be around this point in object-coord-space. + // Also figure out out ratio between object scale factor and multitouch + // diameter at beginning of drag; same for angle and optional anisotropic + // scale. + float currScaleInv = 1.0f / (!mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale); + extractCurrPtInfo(); + startPosX = (mCurrPtX - mCurrXform.xOff) * currScaleInv; + startPosY = (mCurrPtY - mCurrXform.yOff) * currScaleInv; + startScaleOverPinchDiam = mCurrXform.scale / mCurrPtDiam; + startScaleXOverPinchWidth = mCurrXform.scaleX / mCurrPtWidth; + startScaleYOverPinchHeight = mCurrXform.scaleY / mCurrPtHeight; + startAngleMinusPinchAngle = mCurrXform.angle - mCurrPtAng; + } + + /** Drag/stretch/rotate the selected object using the current touch position(s) relative to the anchor position(s). */ + private void performDragOrPinch() { + // Don't do anything if we're not dragging anything + if (selectedObject == null) + return; + + // Calc new position of dragged object + float currScale = !mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale; + extractCurrPtInfo(); + float newPosX = mCurrPtX - startPosX * currScale; + float newPosY = mCurrPtY - startPosY * currScale; + float newScale = startScaleOverPinchDiam * mCurrPtDiam; + float newScaleX = startScaleXOverPinchWidth * mCurrPtWidth; + float newScaleY = startScaleYOverPinchHeight * mCurrPtHeight; + float newAngle = startAngleMinusPinchAngle + mCurrPtAng; + + // Set the new obj coords, scale, and angle as appropriate (notifying the subclass of the change). + mCurrXform.set(newPosX, newPosY, newScale, newScaleX, newScaleY, newAngle); + + boolean success = objectCanvas.setPositionAndScale(selectedObject, mCurrXform, mCurrPt); + if (!success) + ; // If we could't set those params, do nothing currently + } + + /** Indicate if we are in the middle of a pinch action or not. */ + public boolean isPinching() { + return mMode == MODE_PINCH; + } + + + /** + * State-based controller for tracking switches between no-touch, single-touch and multi-touch situations. Includes logic for cleaning up the + * event stream, as events around touch up/down are noisy at least on early Synaptics sensors. + */ + private void multiTouchController() { + if (DEBUG) + Log.i("MultiTouch", "Got here 6 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); + + switch (mMode) { + case MODE_NOTHING: + // Not doing anything currently + if (mCurrPt.isDown()) { + // Start a new single-point drag + selectedObject = objectCanvas.getDraggableObjectAtPoint(mCurrPt); + if (selectedObject != null) { + // Started a new single-point drag + mMode = MODE_DRAG; + objectCanvas.selectObject(selectedObject, mCurrPt); + anchorAtThisPositionAndScale(); + // Don't need any settling time if just placing one finger, there is no noise + mSettleStartTime = mSettleEndTime = mCurrPt.getEventTime(); + } + } + break; + + case MODE_DRAG: + // Currently in a single-point drag + if (!mCurrPt.isDown()) { + // First finger was released, stop dragging + mMode = MODE_NOTHING; + objectCanvas.selectObject((selectedObject = null), mCurrPt); + + } else if (mCurrPt.isMultiTouch()) { + // Point 1 was already down and point 2 was just placed down + mMode = MODE_PINCH; + // Restart the drag with the new drag position (that is at the midpoint between the touchpoints) + anchorAtThisPositionAndScale(); + // Need to let events settle before moving things, to help with event noise on touchdown + mSettleStartTime = mCurrPt.getEventTime(); + mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; + + } else { + // Point 1 is still down and point 2 did not change state, just do single-point drag to new location + if (mCurrPt.getEventTime() < mSettleEndTime) { + // Ignore the first few events if we just stopped stretching, because if finger 2 was kept down while + // finger 1 is lifted, then point 1 gets mapped to finger 2. Restart the drag from the new position. + anchorAtThisPositionAndScale(); + } else { + // Keep dragging, move to new point + performDragOrPinch(); + } + } + break; + + case MODE_PINCH: + // Two-point pinch-scale/rotate/translate + if (!mCurrPt.isMultiTouch() || !mCurrPt.isDown()) { + // Dropped one or both points, stop stretching + + if (!mCurrPt.isDown()) { + // Dropped both points, go back to doing nothing + mMode = MODE_NOTHING; + objectCanvas.selectObject((selectedObject = null), mCurrPt); + + } else { + // Just dropped point 2, downgrade to a single-point drag + mMode = MODE_DRAG; + // Restart the pinch with the single-finger position + anchorAtThisPositionAndScale(); + // Ignore the first few events after the drop, in case we dropped finger 1 and left finger 2 down + mSettleStartTime = mCurrPt.getEventTime(); + mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; + } + + } else { + // Still pinching + if (Math.abs(mCurrPt.getX() - mPrevPt.getX()) > MAX_MULTITOUCH_POS_JUMP_SIZE + || Math.abs(mCurrPt.getY() - mPrevPt.getY()) > MAX_MULTITOUCH_POS_JUMP_SIZE + || Math.abs(mCurrPt.getMultiTouchWidth() - mPrevPt.getMultiTouchWidth()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE + || Math.abs(mCurrPt.getMultiTouchHeight() - mPrevPt.getMultiTouchHeight()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE) { + // Jumped too far, probably event noise, reset and ignore events for a bit + anchorAtThisPositionAndScale(); + mSettleStartTime = mCurrPt.getEventTime(); + mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; + + } else if (mCurrPt.eventTime < mSettleEndTime) { + // Events have not yet settled, reset + anchorAtThisPositionAndScale(); + } else { + // Stretch to new position and size + performDragOrPinch(); + } + } + break; + } + if (DEBUG) + Log.i("MultiTouch", "Got here 7 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); + } + + // ------------------------------------------------------------------------------------ + + /** A class that packages up all MotionEvent information with all derived multitouch information (if available) */ + public static class PointInfo { + // Multitouch information + private int numPoints; + private float[] xs = new float[MAX_TOUCH_POINTS]; + private float[] ys = new float[MAX_TOUCH_POINTS]; + private float[] pressures = new float[MAX_TOUCH_POINTS]; + private int[] pointerIds = new int[MAX_TOUCH_POINTS]; + + // Midpoint of pinch operations + private float xMid, yMid, pressureMid; + + // Width/diameter/angle of pinch operations + private float dx, dy, diameter, diameterSq, angle; + + // Whether or not there is at least one finger down (isDown) and/or at least two fingers down (isMultiTouch) + private boolean isDown, isMultiTouch; + + // Whether or not these fields have already been calculated, for caching purposes + private boolean diameterSqIsCalculated, diameterIsCalculated, angleIsCalculated; + + // Event action code and event time + private int action; + private long eventTime; + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + /** Set all point info */ + private void set(int numPoints, float[] x, float[] y, float[] pressure, int[] pointerIds, int action, boolean isDown, long eventTime) { + if (DEBUG) + Log.i("MultiTouch", "Got here 8 - " + +numPoints + " " + x[0] + " " + y[0] + " " + (numPoints > 1 ? x[1] : x[0]) + " " + + (numPoints > 1 ? y[1] : y[0]) + " " + action + " " + isDown); + this.eventTime = eventTime; + this.action = action; + this.numPoints = numPoints; + for (int i = 0; i < numPoints; i++) { + this.xs[i] = x[i]; + this.ys[i] = y[i]; + this.pressures[i] = pressure[i]; + this.pointerIds[i] = pointerIds[i]; + } + this.isDown = isDown; + this.isMultiTouch = numPoints >= 2; + + if (isMultiTouch) { + xMid = (x[0] + x[1]) * .5f; + yMid = (y[0] + y[1]) * .5f; + pressureMid = (pressure[0] + pressure[1]) * .5f; + dx = Math.abs(x[1] - x[0]); + dy = Math.abs(y[1] - y[0]); + + } else { + // Single-touch event + xMid = x[0]; + yMid = y[0]; + pressureMid = pressure[0]; + dx = dy = 0.0f; + } + // Need to re-calculate the expensive params if they're needed + diameterSqIsCalculated = diameterIsCalculated = angleIsCalculated = false; + } + + /** + * Copy all fields from one PointInfo class to another. PointInfo objects are volatile so you should use this if you want to keep track of the + * last touch event in your own code. + */ + public void set(PointInfo other) { + this.numPoints = other.numPoints; + for (int i = 0; i < numPoints; i++) { + this.xs[i] = other.xs[i]; + this.ys[i] = other.ys[i]; + this.pressures[i] = other.pressures[i]; + this.pointerIds[i] = other.pointerIds[i]; + } + this.xMid = other.xMid; + this.yMid = other.yMid; + this.pressureMid = other.pressureMid; + this.dx = other.dx; + this.dy = other.dy; + this.diameter = other.diameter; + this.diameterSq = other.diameterSq; + this.angle = other.angle; + this.isDown = other.isDown; + this.action = other.action; + this.isMultiTouch = other.isMultiTouch; + this.diameterIsCalculated = other.diameterIsCalculated; + this.diameterSqIsCalculated = other.diameterSqIsCalculated; + this.angleIsCalculated = other.angleIsCalculated; + this.eventTime = other.eventTime; + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + /** True if number of touch points >= 2. */ + public boolean isMultiTouch() { + return isMultiTouch; + } + + /** Difference between x coords of touchpoint 0 and 1. */ + public float getMultiTouchWidth() { + return isMultiTouch ? dx : 0.0f; + } + + /** Difference between y coords of touchpoint 0 and 1. */ + public float getMultiTouchHeight() { + return isMultiTouch ? dy : 0.0f; + } + + /** Fast integer sqrt, by Jim Ulery. Much faster than Math.sqrt() for integers. */ + private int julery_isqrt(int val) { + int temp, g = 0, b = 0x8000, bshft = 15; + do { + if (val >= (temp = (((g << 1) + b) << bshft--))) { + g += b; + val -= temp; + } + } while ((b >>= 1) > 0); + return g; + } + + /** Calculate the squared diameter of the multitouch event, and cache it. Use this if you don't need to perform the sqrt. */ + public float getMultiTouchDiameterSq() { + if (!diameterSqIsCalculated) { + diameterSq = (isMultiTouch ? dx * dx + dy * dy : 0.0f); + diameterSqIsCalculated = true; + } + return diameterSq; + } + + /** Calculate the diameter of the multitouch event, and cache it. Uses fast int sqrt but gives accuracy to 1/16px. */ + public float getMultiTouchDiameter() { + if (!diameterIsCalculated) { + if (!isMultiTouch) { + diameter = 0.0f; + } else { + // Get 1/16 pixel's worth of subpixel accuracy, works on screens up to 2048x2048 + // before we get overflow (at which point you can reduce or eliminate subpix + // accuracy, or use longs in julery_isqrt()) + float diamSq = getMultiTouchDiameterSq(); + diameter = (diamSq == 0.0f ? 0.0f : (float) julery_isqrt((int) (256 * diamSq)) / 16.0f); + // Make sure diameter is never less than dx or dy, for trig purposes + if (diameter < dx) + diameter = dx; + if (diameter < dy) + diameter = dy; + } + diameterIsCalculated = true; + } + return diameter; + } + + /** + * Calculate the angle of a multitouch event, and cache it. Actually gives the smaller of the two angles between the x axis and the line + * between the two touchpoints, so range is [0,Math.PI/2]. Uses Math.atan2(). + */ + public float getMultiTouchAngle() { + if (!angleIsCalculated) { + if (!isMultiTouch) + angle = 0.0f; + else + angle = (float) Math.atan2(ys[1] - ys[0], xs[1] - xs[0]); + angleIsCalculated = true; + } + return angle; + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + /** Return the total number of touch points */ + public int getNumTouchPoints() { + return numPoints; + } + + /** Return the X coord of the first touch point if there's only one, or the midpoint between first and second touch points if two or more. */ + public float getX() { + return xMid; + } + + /** Return the array of X coords -- only the first getNumTouchPoints() of these is defined. */ + public float[] getXs() { + return xs; + } + + /** Return the X coord of the first touch point if there's only one, or the midpoint between first and second touch points if two or more. */ + public float getY() { + return yMid; + } + + /** Return the array of Y coords -- only the first getNumTouchPoints() of these is defined. */ + public float[] getYs() { + return ys; + } + + /** + * Return the array of pointer ids -- only the first getNumTouchPoints() of these is defined. These don't have to be all the numbers from 0 to + * getNumTouchPoints()-1 inclusive, numbers can be skipped if a finger is lifted and the touch sensor is capable of detecting that that + * particular touch point is no longer down. Note that a lot of sensors do not have this capability: when finger 1 is lifted up finger 2 + * becomes the new finger 1. However in theory these IDs can correct for that. Convert back to indices using MotionEvent.findPointerIndex(). + */ + public int[] getPointerIds() { + return pointerIds; + } + + /** Return the pressure the first touch point if there's only one, or the average pressure of first and second touch points if two or more. */ + public float getPressure() { + return pressureMid; + } + + /** Return the array of pressures -- only the first getNumTouchPoints() of these is defined. */ + public float[] getPressures() { + return pressures; + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + public boolean isDown() { + return isDown; + } + + public int getAction() { + return action; + } + + public long getEventTime() { + return eventTime; + } + } + + // ------------------------------------------------------------------------------------ + + /** + * A class that is used to store scroll offsets and scale information for objects that are managed by the multitouch controller + */ + public static class PositionAndScale { + private float xOff, yOff, scale, scaleX, scaleY, angle; + private boolean updateScale, updateScaleXY, updateAngle; + + /** + * Set position and optionally scale, anisotropic scale, and/or angle. Where if the corresponding "update" flag is set to false, the field's + * value will not be changed during a pinch operation. If the value is not being updated *and* the value is not used by the client + * application, then the value can just be zero. However if the value is not being updated but the value *is* being used by the client + * application, the value should still be specified and the update flag should be false (e.g. angle of the object being dragged should still + * be specified even if the program is in "resize" mode rather than "rotate" mode). + */ + public void set(float xOff, float yOff, boolean updateScale, float scale, boolean updateScaleXY, float scaleX, float scaleY, + boolean updateAngle, float angle) { + this.xOff = xOff; + this.yOff = yOff; + this.updateScale = updateScale; + this.scale = scale == 0.0f ? 1.0f : scale; + this.updateScaleXY = updateScaleXY; + this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; + this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; + this.updateAngle = updateAngle; + this.angle = angle; + } + + /** Set position and optionally scale, anisotropic scale, and/or angle, without changing the "update" flags. */ + protected void set(float xOff, float yOff, float scale, float scaleX, float scaleY, float angle) { + this.xOff = xOff; + this.yOff = yOff; + this.scale = scale == 0.0f ? 1.0f : scale; + this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; + this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; + this.angle = angle; + } + + public float getXOff() { + return xOff; + } + + public float getYOff() { + return yOff; + } + + public float getScale() { + return !updateScale ? 1.0f : scale; + } + + /** Included in case you want to support anisotropic scaling */ + public float getScaleX() { + return !updateScaleXY ? 1.0f : scaleX; + } + + /** Included in case you want to support anisotropic scaling */ + public float getScaleY() { + return !updateScaleXY ? 1.0f : scaleY; + } + + public float getAngle() { + return !updateAngle ? 0.0f : angle; + } + } + + // ------------------------------------------------------------------------------------ + + public static interface MultiTouchObjectCanvas { + + /** + * See if there is a draggable object at the current point. Returns the object at the point, or null if nothing to drag. To start a multitouch + * drag/stretch operation, this routine must return some non-null reference to an object. This object is passed into the other methods in this + * interface when they are called. + * + * @param touchPoint + * The point being tested (in object coordinates). Return the topmost object under this point, or if dragging/stretching the whole + * canvas, just return a reference to the canvas. + * @return a reference to the object under the point being tested, or null to cancel the drag operation. If dragging/stretching the whole + * canvas (e.g. in a photo viewer), always return non-null, otherwise the stretch operation won't work. + */ + public T getDraggableObjectAtPoint(PointInfo touchPoint); + + /** + * Get the screen coords of the dragged object's origin, and scale multiplier to convert screen coords to obj coords. The job of this routine + * is to call the .set() method on the passed PositionAndScale object to record the initial position and scale of the object (in object + * coordinates) before any dragging/stretching takes place. + * + * @param obj + * The object being dragged/stretched. + * @param objPosAndScaleOut + * Output parameter: You need to call objPosAndScaleOut.set() to record the current position and scale of obj. + */ + public void getPositionAndScale(T obj, PositionAndScale objPosAndScaleOut); + + /** + * Callback to update the position and scale (in object coords) of the currently-dragged object. + * + * @param obj + * The object being dragged/stretched. + * @param newObjPosAndScale + * The new position and scale of the object, in object coordinates. Use this to move/resize the object before returning. + * @param touchPoint + * Info about the current touch point, including multitouch information and utilities to calculate and cache multitouch pinch + * diameter etc. (Note: touchPoint is volatile, if you want to keep any fields of touchPoint, you must copy them before the method + * body exits.) + * @return true if setting the position and scale of the object was successful, or false if the position or scale parameters are out of range + * for this object. + */ + public boolean setPositionAndScale(T obj, PositionAndScale newObjPosAndScale, PointInfo touchPoint); + + /** + * Select an object at the given point. Can be used to bring the object to top etc. Only called when first touchpoint goes down, not when + * multitouch is initiated. Also called with null on touch-up. + * + * @param obj + * The object being selected by single-touch, or null on touch-up. + * @param touchPoint + * The current touch point. + */ + public void selectObject(T obj, PointInfo touchPoint); + } +} \ No newline at end of file diff --git a/src/main/java/org/osmdroid/DefaultResourceProxyImpl.java b/src/main/java/org/osmdroid/DefaultResourceProxyImpl.java new file mode 100644 index 000000000..1eb9ca9b3 --- /dev/null +++ b/src/main/java/org/osmdroid/DefaultResourceProxyImpl.java @@ -0,0 +1,175 @@ +package org.osmdroid; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import org.osmdroid.views.util.constants.MapViewConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; + +/** + * Default implementation of {@link org.osmdroid.ResourceProxy} that returns fixed string to get + * string resources and reads the jar package to get bitmap resources. + */ +public class DefaultResourceProxyImpl implements ResourceProxy, MapViewConstants { + + private static final Logger logger = LoggerFactory.getLogger(DefaultResourceProxyImpl.class); + + private Resources mResources; + private DisplayMetrics mDisplayMetrics; + + /** + * Constructor. + * + * @param pContext + * Used to get the display metrics that are used for scaling the bitmaps returned by + * {@link #getBitmap} and {@link #getDrawable}. + * Can be null, in which case the bitmaps are not scaled. + */ + public DefaultResourceProxyImpl(final Context pContext) { + if (pContext != null) { + mResources = pContext.getResources(); + mDisplayMetrics = mResources.getDisplayMetrics(); + if (DEBUGMODE) { + logger.debug("mDisplayMetrics=" + mDisplayMetrics); + } + } + } + + @Override + public String getString(final string pResId) { + switch (pResId) { + case mapnik: + return "Mapnik"; + case cyclemap: + return "Cycle Map"; + case public_transport: + return "Public transport"; + case base: + return "OSM base layer"; + case topo: + return "Topographic"; + case hills: + return "Hills"; + case cloudmade_standard: + return "CloudMade (Standard tiles)"; + case cloudmade_small: + return "CloudMade (small tiles)"; + case mapquest_osm: + return "Mapquest"; + case mapquest_aerial: + return "Mapquest Aerial"; + case bing: + return "Bing"; + case fiets_nl: + return "OpenFietsKaart overlay"; + case base_nl: + return "Netherlands base overlay"; + case roads_nl: + return "Netherlands roads overlay"; + case unknown: + return "Unknown"; + case format_distance_meters: + return "%s m"; + case format_distance_kilometers: + return "%s km"; + case format_distance_miles: + return "%s mi"; + case format_distance_nautical_miles: + return "%s nm"; + case format_distance_feet: + return "%s ft"; + case online_mode: + return "Online mode"; + case offline_mode: + return "Offline mode"; + case my_location: + return "My location"; + case compass: + return "Compass"; + case map_mode: + return "Map mode"; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public String getString(final string pResId, final Object... formatArgs) { + return String.format(getString(pResId), formatArgs); + } + + @Override + public Bitmap getBitmap(final bitmap pResId) { + InputStream is = null; + try { + final String resName = pResId.name() + ".png"; + System.out.println(resName); + is = ResourceProxy.class.getResourceAsStream(resName); + if (is == null) { + throw new IllegalArgumentException("Resource not found: " + resName + + ". Did you add the resources directory to the classpath? " + + "I told you to add the damn resource files to the classpath"); + } + BitmapFactory.Options options = null; + if (mDisplayMetrics != null) { + options = getBitmapOptions(); + } + return BitmapFactory.decodeStream(is, null, options); + } catch (final OutOfMemoryError e) { + logger.error("OutOfMemoryError getting bitmap resource: " + pResId); + System.gc(); + // there's not much we can do here + // - when we load a bitmap from resources we expect it to be found + throw e; + } finally { + if (is != null) { + try { + is.close(); + } catch (final IOException ignore) { + } + } + } + } + + private BitmapFactory.Options getBitmapOptions() { + try { + // TODO I think this can all be done without reflection now because all these properties are SDK 4 + final Field density = DisplayMetrics.class.getDeclaredField("DENSITY_DEFAULT"); + final Field inDensity = BitmapFactory.Options.class.getDeclaredField("inDensity"); + final Field inTargetDensity = BitmapFactory.Options.class + .getDeclaredField("inTargetDensity"); + final Field targetDensity = DisplayMetrics.class.getDeclaredField("densityDpi"); + final BitmapFactory.Options options = new BitmapFactory.Options(); + inDensity.setInt(options, density.getInt(null)); + inTargetDensity.setInt(options, targetDensity.getInt(mDisplayMetrics)); + return options; + } catch (final IllegalAccessException ex) { + // ignore + } catch (final NoSuchFieldException ex) { + // ignore + } + return null; + } + + @Override + public Drawable getDrawable(final bitmap pResId) { + return mResources != null + ? new BitmapDrawable(mResources, getBitmap(pResId)) + : new BitmapDrawable(getBitmap(pResId)); + } + + @Override + public float getDisplayMetricsDensity() { + return mDisplayMetrics.density; + } + +} diff --git a/src/main/java/org/osmdroid/LocationListenerProxy.java b/src/main/java/org/osmdroid/LocationListenerProxy.java new file mode 100644 index 000000000..78255af0e --- /dev/null +++ b/src/main/java/org/osmdroid/LocationListenerProxy.java @@ -0,0 +1,63 @@ +package org.osmdroid; + +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; + +public class LocationListenerProxy implements LocationListener { + private final LocationManager mLocationManager; + private LocationListener mListener = null; + + public LocationListenerProxy(final LocationManager pLocationManager) { + mLocationManager = pLocationManager; + } + + public boolean startListening(final LocationListener pListener, final long pUpdateTime, + final float pUpdateDistance) { + boolean result = false; + mListener = pListener; + for (final String provider : mLocationManager.getProviders(true)) { + if (LocationManager.GPS_PROVIDER.equals(provider) + || LocationManager.NETWORK_PROVIDER.equals(provider)) { + result = true; + mLocationManager.requestLocationUpdates(provider, pUpdateTime, pUpdateDistance, + this); + } + } + return result; + } + + public void stopListening() { + mListener = null; + mLocationManager.removeUpdates(this); + } + + @Override + public void onLocationChanged(final Location arg0) { + if (mListener != null) { + mListener.onLocationChanged(arg0); + } + } + + @Override + public void onProviderDisabled(final String arg0) { + if (mListener != null) { + mListener.onProviderDisabled(arg0); + } + } + + @Override + public void onProviderEnabled(final String arg0) { + if (mListener != null) { + mListener.onProviderEnabled(arg0); + } + } + + @Override + public void onStatusChanged(final String arg0, final int arg1, final Bundle arg2) { + if (mListener != null) { + mListener.onStatusChanged(arg0, arg1, arg2); + } + } +} diff --git a/src/main/java/org/osmdroid/ResourceProxy.java b/src/main/java/org/osmdroid/ResourceProxy.java new file mode 100644 index 000000000..604fdee29 --- /dev/null +++ b/src/main/java/org/osmdroid/ResourceProxy.java @@ -0,0 +1,63 @@ +package org.osmdroid; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; + +public interface ResourceProxy { + + public static enum string { + + // tile sources + mapnik, cyclemap, public_transport, base, topo, hills, cloudmade_small, cloudmade_standard, mapquest_osm, mapquest_aerial, bing, + + // overlays + fiets_nl, base_nl, roads_nl, + + // other stuff + unknown, format_distance_meters, format_distance_kilometers, format_distance_miles, format_distance_nautical_miles, format_distance_feet, online_mode, offline_mode, my_location, compass, map_mode, + + } + + public static enum bitmap { + + /** + * For testing - the image doesn't exist. + */ + unknown, + + center, direction_arrow, marker_default, marker_default_focused_base, navto_small, next, previous, person, + + /** + * Menu icons + */ + ic_menu_offline, ic_menu_mylocation, ic_menu_compass, ic_menu_mapmode + } + + String getString(string pResId); + + /** + * Use a string resource as a format definition, and format using the supplied format arguments. + * + * @param pResId + * @param formatArgs + * @return + */ + String getString(string pResId, Object... formatArgs); + + Bitmap getBitmap(bitmap pResId); + + /** + * Get a bitmap as a {@link Drawable} + * + * @param pResId + * @return + */ + Drawable getDrawable(bitmap pResId); + + /** + * Gets the density from the current screen's DisplayMetrics + * + * @return the screen's density + */ + float getDisplayMetricsDensity(); +} diff --git a/src/main/java/org/osmdroid/SensorEventListenerProxy.java b/src/main/java/org/osmdroid/SensorEventListenerProxy.java new file mode 100644 index 000000000..dfaea293d --- /dev/null +++ b/src/main/java/org/osmdroid/SensorEventListenerProxy.java @@ -0,0 +1,44 @@ +package org.osmdroid; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +public class SensorEventListenerProxy implements SensorEventListener { + private final SensorManager mSensorManager; + private SensorEventListener mListener = null; + + public SensorEventListenerProxy(final SensorManager pSensorManager) { + mSensorManager = pSensorManager; + } + + public boolean startListening(final SensorEventListener pListener, final int pSensorType, + final int pRate) { + final Sensor sensor = mSensorManager.getDefaultSensor(pSensorType); + if (sensor == null) + return false; + mListener = pListener; + return mSensorManager.registerListener(this, sensor, pRate); + } + + public void stopListening() { + mListener = null; + mSensorManager.unregisterListener(this); + } + + @Override + public void onAccuracyChanged(final Sensor pSensor, final int pAccuracy) { + if (mListener != null) { + mListener.onAccuracyChanged(pSensor, pAccuracy); + } + } + + @Override + public void onSensorChanged(final SensorEvent pEvent) { + if (mListener != null) { + mListener.onSensorChanged(pEvent); + } + } + +} diff --git a/src/main/java/org/osmdroid/api/IGeoPoint.java b/src/main/java/org/osmdroid/api/IGeoPoint.java new file mode 100644 index 000000000..72323119f --- /dev/null +++ b/src/main/java/org/osmdroid/api/IGeoPoint.java @@ -0,0 +1,11 @@ +package org.osmdroid.api; + +/** + * An interface that resembles the Google Maps API GeoPoint class. + */ +public interface IGeoPoint { + int getLatitudeE6(); + int getLongitudeE6(); + double getLatitude(); + double getLongitude(); +} diff --git a/src/main/java/org/osmdroid/api/IMap.java b/src/main/java/org/osmdroid/api/IMap.java new file mode 100644 index 000000000..b8e47c70f --- /dev/null +++ b/src/main/java/org/osmdroid/api/IMap.java @@ -0,0 +1,107 @@ +package org.osmdroid.api; + +/** + * An interface that contains the common features of osmdroid and Google Maps v2. + */ +public interface IMap { + + /** + * Get the current zoom level of the map + */ + float getZoomLevel(); + + /** + * Set the zoom level of the map + */ + void setZoom(float zoomLevel); + + /** + * Get the center of the map + */ + IGeoPoint getCenter(); + + /** + * Set the center of the map + */ + void setCenter(double latitude, double longitude); + + /** + * Get the bearing of the map. + * Zero means the top of the map is facing north. + */ + float getBearing(); + + /** + * Set the bearing of the map. + * Set to zero for the top of the map to face north. + */ + void setBearing(float bearing); + + /** + * Set the position of the map + */ + void setPosition(IPosition position); + + /** + * Increase zoom level by one + */ + boolean zoomIn(); + + /** + * Decrease zoom level by one + */ + boolean zoomOut(); + + /** + * Whether to show the "my location" dot on the map + */ + void setMyLocationEnabled(boolean enabled); + + /** + * Whether the map is currently showing the "my location" dot + */ + boolean isMyLocationEnabled(); + + /** + * Get the map projection + */ + IProjection getProjection(); + + /** + * Add a marker. + */ + void addMarker(Marker marker); + + /** + * Add a polyline. + * This polyline will be added below other polylines, markers and MyLocationOverlay. + * @return an id that can be used for adding points with {@link #addPointsToPolyline} + */ + int addPolyline(Polyline polyline); + + /** + * Add points to a polyline + * @param id the id returned from {@link #addPolyline(Polyline)} + * @param points the points to add + * @throws IllegalArgumentException if a polyline with this id was not added + */ + void addPointsToPolyline(int id, IGeoPoint... points); + + /** + * Removes one polyline. + * @param id the id returned from {@link #addPolyline(Polyline)} + * @throws IllegalArgumentException if a polyline with this id was not added + * + */ + void clearPolyline(int id); + + /** + * Removes all markers, polylines, polygons, overlays, etc from the map. + */ + void clear(); + + /** + * Sets a callback that's invoked when the map view changes position. + */ + void setOnCameraChangeListener(OnCameraChangeListener listener); +} diff --git a/src/main/java/org/osmdroid/api/IMapController.java b/src/main/java/org/osmdroid/api/IMapController.java new file mode 100644 index 000000000..f7884af43 --- /dev/null +++ b/src/main/java/org/osmdroid/api/IMapController.java @@ -0,0 +1,24 @@ +package org.osmdroid.api; + +import org.osmdroid.views.MapController; + +/** + * An interface that resembles the Google Maps API MapController class and is implemented by the + * osmdroid {@link MapController} class. + * + * @author Neil Boyd + * + */ +public interface IMapController { + void animateTo(IGeoPoint geoPoint); + void scrollBy(int x, int y); + void setCenter(IGeoPoint point); + int setZoom(int zoomLevel); + void stopAnimation(boolean jumpToFinish); + void stopPanning(); + boolean zoomIn(); + boolean zoomInFixing(int xPixel, int yPixel); + boolean zoomOut(); + boolean zoomOutFixing(int xPixel, int yPixel); + void zoomToSpan(int latSpanE6, int lonSpanE6); +} diff --git a/src/main/java/org/osmdroid/api/IMapView.java b/src/main/java/org/osmdroid/api/IMapView.java new file mode 100644 index 000000000..22563b6ba --- /dev/null +++ b/src/main/java/org/osmdroid/api/IMapView.java @@ -0,0 +1,26 @@ +package org.osmdroid.api; + +import org.osmdroid.views.MapView; + +/** + * An interface that resembles the Google Maps API MapView class + * and is implemented by the osmdroid {@link MapView} class. + * + * @author Neil Boyd + * + */ +public interface IMapView { + + IMapController getController(); + IProjection getProjection(); + int getZoomLevel(); + int getMaxZoomLevel(); + int getLatitudeSpan(); + int getLongitudeSpan(); + IGeoPoint getMapCenter(); + + // some methods from View + // (well, just one for now) + void setBackgroundColor(int color); + +} diff --git a/src/main/java/org/osmdroid/api/IMyLocationOverlay.java b/src/main/java/org/osmdroid/api/IMyLocationOverlay.java new file mode 100644 index 000000000..f00967035 --- /dev/null +++ b/src/main/java/org/osmdroid/api/IMyLocationOverlay.java @@ -0,0 +1,33 @@ +package org.osmdroid.api; + +import org.osmdroid.views.overlay.MyLocationOverlay; + +import android.location.Location; +import android.os.Bundle; + +/** + * An interface that resembles the Google Maps API MyLocationOverlay class + * and is implemented by the osmdroid {@link MyLocationOverlay} class. + * + * @author Neil Boyd + * + */ +public interface IMyLocationOverlay { + + boolean enableMyLocation(); + void disableMyLocation(); + boolean isMyLocationEnabled(); + + boolean enableCompass(); + void disableCompass(); + boolean isCompassEnabled() ; + + public float getOrientation(); + + boolean runOnFirstFix(Runnable runnable); + + void onStatusChanged(String provider, int status, Bundle extras); + + Location getLastFix(); + +} diff --git a/src/main/java/org/osmdroid/api/IPosition.java b/src/main/java/org/osmdroid/api/IPosition.java new file mode 100644 index 000000000..2ad5d1596 --- /dev/null +++ b/src/main/java/org/osmdroid/api/IPosition.java @@ -0,0 +1,37 @@ +package org.osmdroid.api; + +/** + * An interface that is used for simultaneously accessing several properties of the map + */ +public interface IPosition { + + /** + * The latitude of the center of the map + */ + double getLatitude(); + + /** + * The longitude of the center of the map + */ + double getLongitude(); + + /** + * Whether this position has a bearing + */ + boolean hasBearing(); + + /** + * The bearing of the map + */ + float getBearing(); + + /** + * Whether this position has a zoom level + */ + boolean hasZoomLevel(); + + /** + * The zoom level of the map + */ + float getZoomLevel(); +} diff --git a/src/main/java/org/osmdroid/api/IProjection.java b/src/main/java/org/osmdroid/api/IProjection.java new file mode 100644 index 000000000..6a33f13d8 --- /dev/null +++ b/src/main/java/org/osmdroid/api/IProjection.java @@ -0,0 +1,56 @@ +package org.osmdroid.api; + +import org.osmdroid.views.MapView.Projection; + +import android.graphics.Point; + +/** + * An interface that resembles the Google Maps API Projection interface and is implemented by the + * osmdroid {@link Projection} class. + * + * @author Neil Boyd + * + */ +public interface IProjection { + + /** + * Converts the given GeoPoint to onscreen pixel coordinates, relative to the top-left of the + * MapView that provided this Projection. + * + * @param in + * The latitude/longitude pair to convert. + * @param out + * A pre-existing object to use for the output; if null, a new Point will be + * allocated and returned. + */ + Point toPixels(IGeoPoint in, Point out); + + /** + * Create a new GeoPoint from pixel coordinates relative to the top-left of the MapView that + * provided this PixelConverter. + */ + IGeoPoint fromPixels(int x, int y); + + /** + * Converts a distance in meters (along the equator) to one in (horizontal) pixels at the + * current zoomlevel. In the default Mercator projection, the actual number of pixels for a + * given distance will get higher as you move away from the equator. + * + * @param meters + * the distance in meters + * @return The number of pixels corresponding to the distance, if measured along the equator, at + * the current zoom level. The return value may only be approximate. + */ + float metersToEquatorPixels(float meters); + + /** + * Get the coordinates of the most north-easterly visible point of the map. + */ + IGeoPoint getNorthEast(); + + /** + * Get the coordinates of the most south-westerly visible point of the map. + */ + IGeoPoint getSouthWest(); + +} diff --git a/src/main/java/org/osmdroid/api/Marker.java b/src/main/java/org/osmdroid/api/Marker.java new file mode 100644 index 000000000..2c076bc4c --- /dev/null +++ b/src/main/java/org/osmdroid/api/Marker.java @@ -0,0 +1,90 @@ +package org.osmdroid.api; + +import android.graphics.Bitmap; + +public class Marker { + + public enum Anchor { + NONE, + CENTER, BOTTOM_CENTER // these are the only two supported by Google Maps v1 + } + + public final double latitude; + + public final double longitude; + + /** + * The title of the marker. If null then marker has no title. + */ + public String title; + + /** + * The title of the marker. If null then marker has no title. + * This method returns the marker for convenient method chaining. + */ + public Marker title(final String aTitle) { + title = aTitle; + return this; + } + + /** + * Snippet displayed below the title. If null then marker has no snippet. + */ + public String snippet; + + /** + * Snippet displayed below the title. If null then marker has no snippet. + * This method returns the marker for convenient method chaining. + */ + public Marker snippet(final String aSnippet) { + snippet = aSnippet; + return this; + } + + /** + * Resource id of marker. If zero then use default marker. + */ + public int icon; + + /** + * Resource id of marker. If zero then use default marker. + * This method returns the marker for convenient method chaining. + */ + public Marker icon(final int aIcon) { + icon = aIcon; + return this; + } + + /** + * Bitmap of marker. If null then use {@link #icon}. + */ + public Bitmap bitmap; + + /** + * Bitmap of marker. If null then use {@link #icon}. + * This method returns the marker for convenient method chaining. + */ + public Marker bitmap(final Bitmap aBitmap) { + bitmap = aBitmap; + return this; + } + + /* + * Anchor of marker. Default is {@link Anchor#BOTTOM_CENTER}. + */ + public Anchor anchor; + + /** + * Anchor of marker. Default is {@link Anchor#BOTTOM_CENTER}. + * This method returns the marker for convenient method chaining. + */ + public Marker anchor(final Anchor aAnchor) { + anchor = aAnchor; + return this; + } + + public Marker(final double aLatitude, final double aLongitude) { + latitude = aLatitude; + longitude = aLongitude; + } +} diff --git a/src/main/java/org/osmdroid/api/OnCameraChangeListener.java b/src/main/java/org/osmdroid/api/OnCameraChangeListener.java new file mode 100644 index 000000000..687db314d --- /dev/null +++ b/src/main/java/org/osmdroid/api/OnCameraChangeListener.java @@ -0,0 +1,10 @@ +package org.osmdroid.api; + +public interface OnCameraChangeListener { + + /** + * Called after the map view has changed. + */ + void onCameraChange (IPosition position); + +} diff --git a/src/main/java/org/osmdroid/api/Polyline.java b/src/main/java/org/osmdroid/api/Polyline.java new file mode 100644 index 000000000..fc47a205f --- /dev/null +++ b/src/main/java/org/osmdroid/api/Polyline.java @@ -0,0 +1,64 @@ +package org.osmdroid.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.graphics.Color; + +public class Polyline { + + public Polyline() { + points = new ArrayList(); + } + + /** + * The color of the polyline. Defaults to black. + */ + public int color = Color.BLACK; + + /** + * The color of the polyline. Defaults to black. + * This method returns the polyline for convenient method chaining. + */ + public Polyline color(final int aColor) { + color = aColor; + return this; + } + + /** + * The width of the polyline. Defaults to 2. + */ + public float width = 2.0f; + + /** + * The width of the polyline. Defaults to 2. + * This method returns the polyline for convenient method chaining. + */ + public Polyline width(final float aWidth) { + width = aWidth; + return this; + } + + /** + * The points of the polyline. + */ + public List points; + + /** + * The points of the polyline. + * This method returns the polyline for convenient method chaining. + */ + public Polyline points(final List aPoints) { + points = aPoints; + return this; + } + + /** + * The points of the polyline. + * This method returns the polyline for convenient method chaining. + */ + public Polyline points(final IGeoPoint... aPoints) { + return points(Arrays.asList(aPoints)); + } +} diff --git a/src/main/java/org/osmdroid/contributor/OSMUploader.java b/src/main/java/org/osmdroid/contributor/OSMUploader.java new file mode 100644 index 000000000..97a867b3c --- /dev/null +++ b/src/main/java/org/osmdroid/contributor/OSMUploader.java @@ -0,0 +1,276 @@ +package org.osmdroid.contributor; + +/** + * Copyright by Christof Dallermassl + * This program is free software and licensed under GPL. + * + * Original JAVA-Code ported for Android compatibility by Nicolas 'plusminus' Gramlich. + */ + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.GregorianCalendar; + +import org.osmdroid.contributor.util.RecordedGeoPoint; +import org.osmdroid.contributor.util.RecordedRouteGPXFormatter; +import org.osmdroid.contributor.util.Util; +import org.osmdroid.contributor.util.constants.OpenStreetMapContributorConstants; + +/** + * Small java class that allows to upload gpx files to www.openstreetmap.org via its api call. + * + * @author cdaller + * @author Nicolas Gramlich + */ +public class OSMUploader implements OpenStreetMapContributorConstants { + + // =========================================================== + // Constants + // =========================================================== + + public static final String API_VERSION = "0.5"; + private static final int BUFFER_SIZE = 65535; + private static final String BASE64_ENC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + private static final String BOUNDARY = "----------------------------d10f7aa230e8"; + private static final String LINE_END = "\r\n"; + + private static final String DEFAULT_DESCRIPTION = "AndNav - automatically created route."; + private static final String DEFAULT_TAGS = "AndNav"; + + public static final SimpleDateFormat pseudoFileNameFormat = new SimpleDateFormat( + "yyyyMMdd'_'HHmmss'_'SSS"); + private static final SimpleDateFormat autoTagFormat = new SimpleDateFormat("MMMM yyyy"); + + // =========================================================== + // Fields + // =========================================================== + + // =========================================================== + // Constructors + // =========================================================== + + /** + * This is a utility class with only static members. + */ + private OSMUploader() { + } + + // =========================================================== + // Methods + // =========================================================== + + /** + * Uses OSMConstants.OSM_USERNAME and OSMConstants.OSM_PASSWORD as username/password. + * Description will be DEFAULT_DESCRIPTION, tags will be automatically generated + * (i.e. "October 2008") NOTE: This method is not blocking! + * + * @param gpxInputStream + * the InputStream containing the gpx-data. + * @throws IOException + */ + public static void uploadAsync(final ArrayList recordedGeoPoints) { + uploadAsync(DEFAULT_DESCRIPTION, DEFAULT_TAGS, true, recordedGeoPoints); + } + + /** + * Uses OSMConstants.OSM_USERNAME and OSMConstants.OSM_PASSWORD as username/password. The + * 'filename' will be the current timestamp.gpx (i.e. "20081231_234815_912.gpx") + * NOTE: This method is not blocking! + * + * @param description + * not null + * @param tags + * not null + * @param addDateTags + * adds Date Tags to the existing Tags (i.e. "October 2008") + * @param gpxInputStreaman + * the InputStream containing the gpx-data. + * @throws IOException + */ + public static void uploadAsync(final String description, final String tags, + final boolean addDateTags, final ArrayList recordedGeoPoints) { + uploadAsync(OSM_USERNAME, OSM_PASSWORD, description, tags, addDateTags, recordedGeoPoints, + pseudoFileNameFormat.format(new GregorianCalendar().getTime()) + "_" + OSM_USERNAME + + ".gpx"); + } + + /** + * NOTE: This method is not blocking! (Code runs in thread) + * + * @param username + * not null and not empty. Valid OSM-username + * @param password + * not null and not empty. Valid password to the + * OSM-username. + * @param description + * not null + * @param tags + * if null addDateTags is treated as true + * @param addDateTags + * adds Date Tags to the existing Tags (i.e. "October 2008") + * @param gpxInputStream + * the InputStream containing the gpx-data. + * @param pseudoFileName + * ending with ".gpx" + * @throws IOException + */ + public static void uploadAsync(final String username, final String password, + final String description, final String tags, final boolean addDateTags, + final ArrayList recordedGeoPoints, final String pseudoFileName) { + if (username == null || username.length() == 0) + return; + if (password == null || password.length() == 0) + return; + if (description == null || description.length() == 0) + return; + if (tags == null || tags.length() == 0) + return; + if (pseudoFileName == null || pseudoFileName.endsWith(".gpx")) + return; + + new Thread(new Runnable() { + @Override + public void run() { + if (!Util.isSufficienDataForUpload(recordedGeoPoints)) + return; + + final InputStream gpxInputStream = new ByteArrayInputStream( + RecordedRouteGPXFormatter.create(recordedGeoPoints).getBytes()); + + String tagsToUse = tags; + if (addDateTags || tagsToUse == null) + if (tagsToUse == null) + tagsToUse = autoTagFormat.format(new GregorianCalendar().getTime()); + else + tagsToUse = tagsToUse + " " + + autoTagFormat.format(new GregorianCalendar().getTime()); + + // logger.debug("Uploading " + pseudoFileName + " to openstreetmap.org"); + try { + // String urlGpxName = URLEncoder.encode(gpxName.replaceAll("\\.;&?,/","_"), + // "UTF-8"); + final String urlDesc = (description == null) ? DEFAULT_DESCRIPTION + : description.replaceAll("\\.;&?,/", "_"); + final String urlTags = (tagsToUse == null) ? DEFAULT_TAGS + : tagsToUse.replaceAll("\\\\.;&?,/", "_"); + final URL url = new URL("http://www.openstreetmap.org/api/" + API_VERSION + + "/gpx/create"); + // logger.debug("Destination Url: " + url); + final HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setConnectTimeout(15000); + con.setRequestMethod("POST"); + con.setDoOutput(true); + con.addRequestProperty("Authorization", "Basic " + + encodeBase64(username + ":" + password)); + con.addRequestProperty("Content-Type", "multipart/form-data; boundary=" + + BOUNDARY); + con.addRequestProperty("Connection", "close"); // counterpart of keep-alive + con.addRequestProperty("Expect", ""); + + con.connect(); + final DataOutputStream out = new DataOutputStream(new BufferedOutputStream( + con.getOutputStream())); + // DataOutputStream out = new DataOutputStream(System.out); + + writeContentDispositionFile(out, "file", gpxInputStream, pseudoFileName); + writeContentDisposition(out, "description", urlDesc); + writeContentDisposition(out, "tags", urlTags); + + writeContentDisposition(out, "public", "1"); + + out.writeBytes("--" + BOUNDARY + "--" + LINE_END); + out.flush(); + + final int retCode = con.getResponseCode(); + String retMsg = con.getResponseMessage(); + // logger.debug("\nreturn code: "+retCode + " " + retMsg); + if (retCode != 200) { + // Look for a detailed error message from the server + if (con.getHeaderField("Error") != null) + retMsg += "\n" + con.getHeaderField("Error"); + con.disconnect(); + throw new RuntimeException(retCode + " " + retMsg); + } + out.close(); + con.disconnect(); + } catch (final Exception e) { + // logger.error("OSMUpload Error", e); + } + } + + }, "OSMUpload-Thread").start(); + } + + public static void upload(final String username, final String password, + final String description, final String tags, final boolean addDateTags, + final ArrayList recordedGeoPoints, final String pseudoFileName) + throws IOException { + uploadAsync(username, password, description, tags, addDateTags, recordedGeoPoints, + pseudoFileName); + } + + /** + * @param out + * @param string + * @param gpxFile + * @throws IOException + */ + private static void writeContentDispositionFile(final DataOutputStream out, final String name, + final InputStream gpxInputStream, final String pseudoFileName) throws IOException { + out.writeBytes("--" + BOUNDARY + LINE_END); + out.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + + pseudoFileName + "\"" + LINE_END); + out.writeBytes("Content-Type: application/octet-stream" + LINE_END); + out.writeBytes(LINE_END); + + final byte[] buffer = new byte[BUFFER_SIZE]; + // int fileLen = (int)gpxFile.length(); + int read; + int sumread = 0; + final InputStream in = new BufferedInputStream(gpxInputStream); + // logger.debug("Transferring data to server"); + while ((read = in.read(buffer)) >= 0) { + out.write(buffer, 0, read); + out.flush(); + sumread += read; + } + in.close(); + out.writeBytes(LINE_END); + } + + /** + * @param string + * @param urlDesc + * @throws IOException + */ + private static void writeContentDisposition(final DataOutputStream out, final String name, + final String value) throws IOException { + out.writeBytes("--" + BOUNDARY + LINE_END); + out.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"" + LINE_END); + out.writeBytes(LINE_END); + out.writeBytes(value + LINE_END); + } + + private static String encodeBase64(final String s) { + final StringBuilder out = new StringBuilder(); + for (int i = 0; i < (s.length() + 2) / 3; ++i) { + final int l = Math.min(3, s.length() - i * 3); + final String buf = s.substring(i * 3, i * 3 + l); + out.append(BASE64_ENC.charAt(buf.charAt(0) >> 2)); + out.append(BASE64_ENC.charAt((buf.charAt(0) & 0x03) << 4 + | (l == 1 ? 0 : (buf.charAt(1) & 0xf0) >> 4))); + out.append(l > 1 ? BASE64_ENC.charAt((buf.charAt(1) & 0x0f) << 2 + | (l == 2 ? 0 : (buf.charAt(2) & 0xc0) >> 6)) : '='); + out.append(l > 2 ? BASE64_ENC.charAt(buf.charAt(2) & 0x3f) : '='); + } + return out.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/osmdroid/contributor/RouteRecorder.java b/src/main/java/org/osmdroid/contributor/RouteRecorder.java new file mode 100644 index 000000000..ab03a4e2d --- /dev/null +++ b/src/main/java/org/osmdroid/contributor/RouteRecorder.java @@ -0,0 +1,61 @@ +// Created by plusminus on 12:28:16 - 21.09.2008 +package org.osmdroid.contributor; + +import java.util.ArrayList; + +import org.osmdroid.contributor.util.RecordedGeoPoint; +import org.osmdroid.util.GeoPoint; + +import android.location.Location; + +/** + * + * @author Nicolas Gramlich + * + */ +public class RouteRecorder { + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected final ArrayList mRecords = new ArrayList(); + + // =========================================================== + // Constructors + // =========================================================== + + // =========================================================== + // Getter & Setter + // =========================================================== + + public ArrayList getRecordedGeoPoints() { + return this.mRecords; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + public void add(final Location aLocation, final int aNumSatellites) { + this.mRecords + .add(new RecordedGeoPoint((int) (aLocation.getLatitude() * 1E6), (int) (aLocation + .getLongitude() * 1E6), System.currentTimeMillis(), aNumSatellites)); + } + + public void add(final GeoPoint aGeoPoint, final int aNumSatellites) { + this.mRecords.add(new RecordedGeoPoint(aGeoPoint.getLatitudeE6(), aGeoPoint + .getLongitudeE6(), System.currentTimeMillis(), aNumSatellites)); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/contributor/util/RecordedGeoPoint.java b/src/main/java/org/osmdroid/contributor/util/RecordedGeoPoint.java new file mode 100644 index 000000000..cf86353a9 --- /dev/null +++ b/src/main/java/org/osmdroid/contributor/util/RecordedGeoPoint.java @@ -0,0 +1,73 @@ +// Created by plusminus on 12:29:23 - 21.09.2008 +package org.osmdroid.contributor.util; + +import org.osmdroid.contributor.util.constants.OpenStreetMapContributorConstants; +import org.osmdroid.util.GeoPoint; + +/** + * Extends the {@link GeoPoint} with a timeStamp. + * + * @author Nicolas Gramlich + */ +public class RecordedGeoPoint extends GeoPoint implements OpenStreetMapContributorConstants { + + // =========================================================== + // Constants + // =========================================================== + + private static final long serialVersionUID = 7304941424576720318L; + + // =========================================================== + // Fields + // =========================================================== + + protected final long mTimeStamp; + protected final int mNumSatellites; + + // =========================================================== + // Constructors + // =========================================================== + + public RecordedGeoPoint(final int latitudeE6, final int longitudeE6) { + this(latitudeE6, longitudeE6, System.currentTimeMillis(), NOT_SET); + } + + public RecordedGeoPoint(final int latitudeE6, final int longitudeE6, final long aTimeStamp, + final int aNumSatellites) { + super(latitudeE6, longitudeE6); + this.mTimeStamp = aTimeStamp; + this.mNumSatellites = aNumSatellites; + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public long getTimeStamp() { + return this.mTimeStamp; + } + + public double getLatitudeAsDouble() { + return this.getLatitudeE6() / 1E6; + } + + public double getLongitudeAsDouble() { + return this.getLongitudeE6() / 1E6; + } + + public int getNumSatellites() { + return this.mNumSatellites; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/contributor/util/RecordedRouteGPXFormatter.java b/src/main/java/org/osmdroid/contributor/util/RecordedRouteGPXFormatter.java new file mode 100644 index 000000000..18167e43a --- /dev/null +++ b/src/main/java/org/osmdroid/contributor/util/RecordedRouteGPXFormatter.java @@ -0,0 +1,131 @@ +// Created by plusminus on 13:23:45 - 21.09.2008 +package org.osmdroid.contributor.util; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Formatter; +import java.util.List; + +import org.osmdroid.contributor.util.constants.OpenStreetMapContributorConstants; + +/** + * Class capable of formatting a List of Points to the GPX 1.1 format. + * + * @author Nicolas Gramlich + * + */ +public class RecordedRouteGPXFormatter implements OpenStreetMapContributorConstants { + + // =========================================================== + // Constants + // =========================================================== + + private static final String XML_VERSION = ""; + private static final String GPX_VERSION = "1.1"; + private static final String GPX_TAG = ""; + private static final String GPX_TAG_CLOSE = ""; + private static final String GPX_TAG_TIME = ""; + private static final String GPX_TAG_TRACK = ""; + private static final String GPX_TAG_TRACK_CLOSE = ""; + private static final String GPX_TAG_TRACK_NAME = "%s"; + private static final String GPX_TAG_TRACK_SEGMENT = ""; + private static final String GPX_TAG_TRACK_SEGMENT_CLOSE = ""; + public static final String GPX_TAG_TRACK_SEGMENT_POINT = ""; + public static final String GPX_TAG_TRACK_SEGMENT_POINT_CLOSE = ""; + public static final String GPX_TAG_TRACK_SEGMENT_POINT_TIME = ""; + public static final String GPX_TAG_TRACK_SEGMENT_POINT_SAT = "%d"; + public static final String GPX_TAG_TRACK_SEGMENT_POINT_ELE = "%d"; + + private static final SimpleDateFormat formatterCompleteDateTime = new SimpleDateFormat( + "yyyyMMdd'_'HHmmss"); + + // =========================================================== + // Fields + // =========================================================== + + // =========================================================== + // Constructors + // =========================================================== + + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + /** + * Creates a String in the following XML format: + * + *
+	 * <?xml version="1.0"?>
+	 * <gpx version="1.1" creator="AndNav - http://www.andnav.org - Android Navigation System" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
+	 * 	<time>2008-09-22T00:46:20Z</time>
+	 * 	<trk>
+	 * 	<name>plusminus--yyyyMMdd_HHmmss-yyyyMMdd_HHmmss</name>
+	 * 		<trkseg>
+	 * 			<trkpt lat="49.472767" lon="8.654174">
+	 * 				<time>2008-09-22T00:46:20Z</time>
+	 * 			</trkpt>
+	 * 			<trkpt lat="49.472797" lon="8.654102">
+	 * 				<time>2008-09-22T00:46:35Z</time>
+	 * 			</trkpt>
+	 * 			<trkpt lat="49.472802" lon="8.654185">
+	 * 				<time>2008-09-22T00:46:50Z</time>
+	 * 			</trkpt>
+	 * 		</trkseg>
+	 * 	</trk>
+	 * </gpx>
+	 * 
+ * + */ + public static String create(final List someRecords) + throws IllegalArgumentException { + if (someRecords == null) + throw new IllegalArgumentException("Records may not be null."); + + if (someRecords.size() == 0) + throw new IllegalArgumentException("Records size == 0"); + + final StringBuilder sb = new StringBuilder(); + final Formatter f = new Formatter(sb); + sb.append(XML_VERSION); + f.format(GPX_TAG, OSM_CREATOR_INFO); + f.format(GPX_TAG_TIME, Util.convertTimestampToUTCString(System.currentTimeMillis())); + sb.append(GPX_TAG_TRACK); + f.format( + GPX_TAG_TRACK_NAME, + OSM_USERNAME + + "--" + + formatterCompleteDateTime.format(new Date(someRecords.get(0) + .getTimeStamp()).getTime()) + + "-" + + formatterCompleteDateTime.format(new Date(someRecords.get( + someRecords.size() - 1).getTimeStamp()).getTime())); + sb.append(GPX_TAG_TRACK_SEGMENT); + + for (final RecordedGeoPoint rgp : someRecords) { + f.format(GPX_TAG_TRACK_SEGMENT_POINT, rgp.getLatitudeAsDouble(), + rgp.getLongitudeAsDouble()); + f.format(GPX_TAG_TRACK_SEGMENT_POINT_TIME, + Util.convertTimestampToUTCString(rgp.getTimeStamp())); + if (rgp.mNumSatellites != NOT_SET) + f.format(GPX_TAG_TRACK_SEGMENT_POINT_SAT, rgp.mNumSatellites); + sb.append(GPX_TAG_TRACK_SEGMENT_POINT_CLOSE); + } + + sb.append(GPX_TAG_TRACK_SEGMENT_CLOSE).append(GPX_TAG_TRACK_CLOSE).append(GPX_TAG_CLOSE); + + return sb.toString(); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/contributor/util/Util.java b/src/main/java/org/osmdroid/contributor/util/Util.java new file mode 100644 index 000000000..ebbce7621 --- /dev/null +++ b/src/main/java/org/osmdroid/contributor/util/Util.java @@ -0,0 +1,78 @@ +// Created by plusminus on 13:24:05 - 21.09.2008 +package org.osmdroid.contributor.util; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.TimeZone; + +import org.osmdroid.contributor.util.constants.OpenStreetMapContributorConstants; +import org.osmdroid.util.BoundingBoxE6; + +/** + * + * @author Nicolas Gramlich + * + */ +public class Util implements OpenStreetMapContributorConstants { + + // =========================================================== + // Constants + // =========================================================== + + public static final SimpleDateFormat UTCSimpleDateFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss'Z'"); + { + UTCSimpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + // =========================================================== + // Fields + // =========================================================== + + // =========================================================== + // Constructors + // =========================================================== + + /** + * This is a utility class with only static members. + */ + private Util() { + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + public static final String convertTimestampToUTCString(final long aTimestamp) { + return UTCSimpleDateFormat.format(new Date(aTimestamp)); + } + + public static boolean isSufficienDataForUpload( + final ArrayList recordedGeoPoints) { + if (recordedGeoPoints == null) + return false; + + if (recordedGeoPoints.size() < MINGEOPOINTS_FOR_OSM_CONTRIBUTION) + return false; + + final BoundingBoxE6 bb = BoundingBoxE6.fromGeoPoints(recordedGeoPoints); + final int diagMeters = bb.getDiagonalLengthInMeters(); + if (diagMeters < MINDIAGONALMETERS_FOR_OSM_CONTRIBUTION) + return false; + + return true; + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/contributor/util/constants/OpenStreetMapContributorConstants.java b/src/main/java/org/osmdroid/contributor/util/constants/OpenStreetMapContributorConstants.java new file mode 100644 index 000000000..d32ff85c6 --- /dev/null +++ b/src/main/java/org/osmdroid/contributor/util/constants/OpenStreetMapContributorConstants.java @@ -0,0 +1,30 @@ +// Created by plusminus on 14:11:09 - 21.09.2008 +package org.osmdroid.contributor.util.constants; + +/** + * + * This class contains constants used by the contributor package. + * + * @author Nicolas Gramlich + * + */ +public interface OpenStreetMapContributorConstants { + + // =========================================================== + // Final Fields + // =========================================================== + + public static final int NOT_SET = Integer.MIN_VALUE; + + public static final String OSM_USERNAME = "PUT_YOUR_USERNAME_HERE"; + public static final String OSM_PASSWORD = "PUT_YOUR_PASSWORD_HERE"; + + public static final int MINGEOPOINTS_FOR_OSM_CONTRIBUTION = 100; + public static final int MINDIAGONALMETERS_FOR_OSM_CONTRIBUTION = 300; + + public static final String OSM_CREATOR_INFO = "AndNav - http://www.andnav.org - Android Navigation System"; + + // =========================================================== + // Methods + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/events/DelayedMapListener.java b/src/main/java/org/osmdroid/events/DelayedMapListener.java new file mode 100644 index 000000000..054d0df2c --- /dev/null +++ b/src/main/java/org/osmdroid/events/DelayedMapListener.java @@ -0,0 +1,100 @@ +package org.osmdroid.events; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.os.Handler; + +/* + * A MapListener that aggregates multiple events called in quick succession. + * After an event arrives, if another event arrives within delay milliseconds, + * the original event is discarded. Otherwise, the event is propagated to the wrapped + * MapListener. Note: This class is not thread-safe. + * + * @author Theodore Hong + */ +public class DelayedMapListener implements MapListener { + + private static final Logger logger = LoggerFactory.getLogger(DelayedMapListener.class); + + /** Default listening delay */ + protected static final int DEFAULT_DELAY = 100; + + /** The wrapped MapListener */ + MapListener wrappedListener; + + /** Listening delay, in milliseconds */ + protected long delay; + + protected Handler handler; + protected CallbackTask callback; + + /* + * @param wrappedListener The wrapped MapListener + * + * @param delay Listening delay, in milliseconds + */ + public DelayedMapListener(final MapListener wrappedListener, final long delay) { + this.wrappedListener = wrappedListener; + this.delay = delay; + this.handler = new Handler(); + this.callback = null; + } + + /* + * Constructor with default delay. + * + * @param wrappedListener The wrapped MapListener + */ + public DelayedMapListener(final MapListener wrappedListener) { + this(wrappedListener, DEFAULT_DELAY); + } + + @Override + public boolean onScroll(final ScrollEvent event) { + dispatch(event); + return true; + } + + @Override + public boolean onZoom(final ZoomEvent event) { + dispatch(event); + return true; + } + + /* + * Process an incoming MapEvent. + */ + protected void dispatch(final MapEvent event) { + // cancel any pending callback + if (callback != null) { + handler.removeCallbacks(callback); + } + callback = new CallbackTask(event); + + // set timer + handler.postDelayed(callback, delay); + } + + // Callback tasks + private class CallbackTask implements Runnable { + private final MapEvent event; + + public CallbackTask(final MapEvent event) { + this.event = event; + } + + @Override + public void run() { + // do the callback + if (event instanceof ScrollEvent) { + wrappedListener.onScroll((ScrollEvent) event); + } else if (event instanceof ZoomEvent) { + wrappedListener.onZoom((ZoomEvent) event); + } else { + // unknown event; discard + logger.debug("Unknown event received: " + event); + } + } + } +} diff --git a/src/main/java/org/osmdroid/events/MapAdapter.java b/src/main/java/org/osmdroid/events/MapAdapter.java new file mode 100644 index 000000000..53f02c5eb --- /dev/null +++ b/src/main/java/org/osmdroid/events/MapAdapter.java @@ -0,0 +1,21 @@ +package org.osmdroid.events; + +/* + * An abstract adapter class for receiving map events. The methods in this class are empty. + * This class exists as convenience for creating listener objects. + * + * @author Theodore Hong + */ +public abstract class MapAdapter implements MapListener { + @Override + public boolean onScroll(final ScrollEvent event) { + // do nothing + return false; + } + + @Override + public boolean onZoom(final ZoomEvent event) { + // do nothing + return false; + } +} diff --git a/src/main/java/org/osmdroid/events/MapEvent.java b/src/main/java/org/osmdroid/events/MapEvent.java new file mode 100644 index 000000000..ff1a64ac2 --- /dev/null +++ b/src/main/java/org/osmdroid/events/MapEvent.java @@ -0,0 +1,10 @@ +package org.osmdroid.events; + +/* + * Tagging interface for map events + * + * @author Theodore Hong + */ +public interface MapEvent { + +} diff --git a/src/main/java/org/osmdroid/events/MapListener.java b/src/main/java/org/osmdroid/events/MapListener.java new file mode 100644 index 000000000..26ac4229a --- /dev/null +++ b/src/main/java/org/osmdroid/events/MapListener.java @@ -0,0 +1,20 @@ +package org.osmdroid.events; + +/* + * The listener interface for receiving map movement events. To process a map event, either implement + * this interface or extend MapAdapter, then register with the MapView using + * setMapListener. + * + * @author Theodore Hong + */ +public interface MapListener { + /* + * Called when a map is scrolled. + */ + public boolean onScroll(ScrollEvent event); + + /* + * Called when a map is zoomed. + */ + public boolean onZoom(ZoomEvent event); +} diff --git a/src/main/java/org/osmdroid/events/ScrollEvent.java b/src/main/java/org/osmdroid/events/ScrollEvent.java new file mode 100644 index 000000000..67215aacf --- /dev/null +++ b/src/main/java/org/osmdroid/events/ScrollEvent.java @@ -0,0 +1,46 @@ +package org.osmdroid.events; + +import org.osmdroid.views.MapView; + +/* + * The event generated when a map has finished scrolling to the coordinates (x,y). + * + * @author Theodore Hong + */ +public class ScrollEvent implements MapEvent { + protected MapView source; + protected int x; + protected int y; + + public ScrollEvent(final MapView source, final int x, final int y) { + this.source = source; + this.x = x; + this.y = y; + } + + /* + * Return the map which generated this event. + */ + public MapView getSource() { + return source; + } + + /* + * Return the x-coordinate scrolled to. + */ + public int getX() { + return x; + } + + /* + * Return the y-coordinate scrolled to. + */ + public int getY() { + return y; + } + + @Override + public String toString() { + return "ScrollEvent [source=" + source + ", x=" + x + ", y=" + y + "]"; + } +} diff --git a/src/main/java/org/osmdroid/events/ZoomEvent.java b/src/main/java/org/osmdroid/events/ZoomEvent.java new file mode 100644 index 000000000..880e5db42 --- /dev/null +++ b/src/main/java/org/osmdroid/events/ZoomEvent.java @@ -0,0 +1,37 @@ +package org.osmdroid.events; + +import org.osmdroid.views.MapView; + +/* + * The event generated when a map has finished zooming to the level zoomLevel. + * + * @author Theodore Hong + */ +public class ZoomEvent implements MapEvent { + protected MapView source; + protected int zoomLevel; + + public ZoomEvent(final MapView source, final int zoomLevel) { + this.source = source; + this.zoomLevel = zoomLevel; + } + + /* + * Return the map which generated this event. + */ + public MapView getSource() { + return source; + } + + /* + * Return the zoom level zoomed to. + */ + public int getZoomLevel() { + return zoomLevel; + } + + @Override + public String toString() { + return "ZoomEvent [source=" + source + ", zoomLevel=" + zoomLevel + "]"; + } +} diff --git a/src/main/java/org/osmdroid/http/HttpClientFactory.java b/src/main/java/org/osmdroid/http/HttpClientFactory.java new file mode 100644 index 000000000..2d0252381 --- /dev/null +++ b/src/main/java/org/osmdroid/http/HttpClientFactory.java @@ -0,0 +1,39 @@ +package org.osmdroid.http; + +import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.DefaultHttpClient; + +/** + * Factory class for creating an instance of {@link HttpClient}. + * The default implementation returns an instance of {@link DefaultHttpClient}. + * In order to use a different implementation call {@link #setFactoryInstance(IHttpClientFactory)} + * early in your code, for example in onCreate in your main activity. + * For example to use + * OkHttp/ + * use the following code + * + * HttpClientFactory.setFactoryInstance(new IHttpClientFactory() { + * public HttpClient createHttpClient() { + * return new OkApacheClient(); + * } + * }); + * + */ +public class HttpClientFactory { + + private static IHttpClientFactory mFactoryInstance = new IHttpClientFactory() { + @Override + public HttpClient createHttpClient() { + return new DefaultHttpClient(); + } + }; + + public static void setFactoryInstance(final IHttpClientFactory aHttpClientFactory) { + mFactoryInstance = aHttpClientFactory; + } + + public static HttpClient createHttpClient() { + return mFactoryInstance.createHttpClient(); + } + +} diff --git a/src/main/java/org/osmdroid/http/IHttpClientFactory.java b/src/main/java/org/osmdroid/http/IHttpClientFactory.java new file mode 100644 index 000000000..11148b365 --- /dev/null +++ b/src/main/java/org/osmdroid/http/IHttpClientFactory.java @@ -0,0 +1,16 @@ +package org.osmdroid.http; + +import org.apache.http.client.HttpClient; + +/** + * Factory class for creating an instance of {@link HttpClient}. + * See {@link HttpClientFactory} for usage. + */ +public interface IHttpClientFactory { + + /** + * Create an instance of {@link HttpClient}. + */ + HttpClient createHttpClient(); + +} diff --git a/src/main/java/org/osmdroid/tileprovider/BitmapPool.java b/src/main/java/org/osmdroid/tileprovider/BitmapPool.java new file mode 100644 index 000000000..633a3ade7 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/BitmapPool.java @@ -0,0 +1,65 @@ +package org.osmdroid.tileprovider; + +import java.util.LinkedList; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; + +public class BitmapPool { + final LinkedList mPool = new LinkedList(); + + private static BitmapPool sInstance; + + public static BitmapPool getInstance() { + if (sInstance == null) + sInstance = new BitmapPool(); + + return sInstance; + } + + public void returnDrawableToPool(ReusableBitmapDrawable drawable) { + Bitmap b = drawable.tryRecycle(); + if (b != null && b.isMutable()) + synchronized (mPool) { + mPool.addLast(b); + } + } + + public void applyReusableOptions(BitmapFactory.Options bitmapOptions) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + Bitmap pooledBitmap = obtainBitmapFromPool(); + bitmapOptions.inBitmap = pooledBitmap; + bitmapOptions.inSampleSize = 1; + bitmapOptions.inMutable = true; + } + } + + public Bitmap obtainBitmapFromPool() { + final Bitmap b; + synchronized (mPool) { + if (mPool.size() == 0) + return null; + else + b = mPool.removeFirst(); + } + + return b; + } + + public Bitmap obtainSizedBitmapFromPool(int width, int height) { + synchronized (mPool) { + if (mPool.size() == 0) + return null; + else { + for (Bitmap bitmap : mPool) + if (bitmap.getWidth() == width && bitmap.getHeight() == height) { + mPool.remove(bitmap); + return bitmap; + } + } + } + + return null; + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/ExpirableBitmapDrawable.java b/src/main/java/org/osmdroid/tileprovider/ExpirableBitmapDrawable.java new file mode 100644 index 000000000..14f3c23dd --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/ExpirableBitmapDrawable.java @@ -0,0 +1,50 @@ +package org.osmdroid.tileprovider; + +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +/** + * A {@link BitmapDrawable} for a {@link MapTile} that has a state to indicate that it's expired. + */ +public class ExpirableBitmapDrawable extends BitmapDrawable { + + public static final int EXPIRED = -1; + + private int[] mState; + + public ExpirableBitmapDrawable(final Bitmap pBitmap) { + super(pBitmap); + mState = new int[0]; + } + + @Override + public int[] getState() { + return mState; + } + + @Override + public boolean isStateful() { + return mState.length > 0; + } + + @Override + public boolean setState(final int[] pStateSet) { + mState = pStateSet; + return true; + } + + public static boolean isDrawableExpired(final Drawable pTile) { + if (!pTile.isStateful()) { + return false; + } + final int[] state = pTile.getState(); + for(int i = 0; i < state.length; i++) { + if (state[i] == EXPIRED) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/IMapTileProviderCallback.java b/src/main/java/org/osmdroid/tileprovider/IMapTileProviderCallback.java new file mode 100644 index 000000000..0bfd06910 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/IMapTileProviderCallback.java @@ -0,0 +1,39 @@ +package org.osmdroid.tileprovider; + +import android.graphics.drawable.Drawable; + +public interface IMapTileProviderCallback { + + /** + * The map tile request has completed. + * + * @param aState + * a state object + * @param aDrawable + * a drawable + */ + void mapTileRequestCompleted(MapTileRequestState aState, final Drawable aDrawable); + + /** + * The map tile request has failed. + * + * @param aState + * a state object + */ + void mapTileRequestFailed(MapTileRequestState aState); + + /** + * The map tile request has produced an expired tile. + * + * @param aState + * a state object + */ + void mapTileRequestExpiredTile(MapTileRequestState aState, final Drawable aDrawable); + + /** + * Returns true if the network connection should be used, false if not. + * + * @return true if data connection should be used, false otherwise + */ + public boolean useDataConnection(); +} \ No newline at end of file diff --git a/src/main/java/org/osmdroid/tileprovider/IRegisterReceiver.java b/src/main/java/org/osmdroid/tileprovider/IRegisterReceiver.java new file mode 100644 index 000000000..4c5cc4f6c --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/IRegisterReceiver.java @@ -0,0 +1,12 @@ +package org.osmdroid.tileprovider; + +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; + +public interface IRegisterReceiver { + + Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter); + + void unregisterReceiver(BroadcastReceiver receiver); +} diff --git a/src/main/java/org/osmdroid/tileprovider/LRUMapTileCache.java b/src/main/java/org/osmdroid/tileprovider/LRUMapTileCache.java new file mode 100644 index 000000000..61e2b2c4d --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/LRUMapTileCache.java @@ -0,0 +1,90 @@ +package org.osmdroid.tileprovider; + +import java.util.LinkedHashMap; + +import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; + +public class LRUMapTileCache extends LinkedHashMap + implements OpenStreetMapTileProviderConstants { + + public interface TileRemovedListener { + void onTileRemoved(MapTile mapTile); + } + + private static final Logger logger = LoggerFactory.getLogger(LRUMapTileCache.class); + + private static final long serialVersionUID = -541142277575493335L; + + private int mCapacity; + private TileRemovedListener mTileRemovedListener; + + public LRUMapTileCache(final int aCapacity) { + super(aCapacity + 2, 0.1f, true); + mCapacity = aCapacity; + } + + public void ensureCapacity(final int aCapacity) { + if (aCapacity > mCapacity) { + logger.info("Tile cache increased from " + mCapacity + " to " + aCapacity); + mCapacity = aCapacity; + } + } + + @Override + public Drawable remove(final Object aKey) { + final Drawable drawable = super.remove(aKey); + // Only recycle if we are running on a project less than 2.3.3 Gingerbread. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { + if (drawable instanceof BitmapDrawable) { + final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); + if (bitmap != null) { + bitmap.recycle(); + } + } + } + if (getTileRemovedListener() != null && aKey instanceof MapTile) + getTileRemovedListener().onTileRemoved((MapTile) aKey); + if (drawable instanceof ReusableBitmapDrawable) + BitmapPool.getInstance().returnDrawableToPool((ReusableBitmapDrawable) drawable); + return drawable; + } + + @Override + public void clear() { + // remove them all individually so that they get recycled + while (!isEmpty()) { + remove(keySet().iterator().next()); + } + + // and then clear + super.clear(); + } + + @Override + protected boolean removeEldestEntry(final java.util.Map.Entry aEldest) { + if (size() > mCapacity) { + final MapTile eldest = aEldest.getKey(); + if (DEBUGMODE) { + logger.debug("Remove old tile: " + eldest); + } + remove(eldest); + // don't return true because we've already removed it + } + return false; + } + + public TileRemovedListener getTileRemovedListener() { + return mTileRemovedListener; + } + + public void setTileRemovedListener(TileRemovedListener tileRemovedListener) { + mTileRemovedListener = tileRemovedListener; + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/MapTile.java b/src/main/java/org/osmdroid/tileprovider/MapTile.java new file mode 100644 index 000000000..a2ffb931b --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/MapTile.java @@ -0,0 +1,66 @@ +package org.osmdroid.tileprovider; + +import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase; +import org.osmdroid.views.overlay.TilesOverlay; + +/** + * A map tile is distributed using the observer pattern. The tile is delivered by a tile provider + * (i.e. a descendant of {@link MapTileModuleProviderBase} or + * {@link MapTileProviderBase} to a consumer of tiles (e.g. descendant of + * {@link TilesOverlay}). Tiles are typically images (e.g. png or jpeg). + */ +public class MapTile { + + public static final int MAPTILE_SUCCESS_ID = 0; + public static final int MAPTILE_FAIL_ID = MAPTILE_SUCCESS_ID + 1; + + // This class must be immutable because it's used as the key in the cache hash map + // (ie all the fields are final). + private final int x; + private final int y; + private final int zoomLevel; + + public MapTile(final int zoomLevel, final int tileX, final int tileY) { + this.zoomLevel = zoomLevel; + this.x = tileX; + this.y = tileY; + } + + public int getZoomLevel() { + return zoomLevel; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public String toString() { + return "/" + zoomLevel + "/" + x + "/" + y; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (!(obj instanceof MapTile)) + return false; + final MapTile rhs = (MapTile) obj; + return zoomLevel == rhs.zoomLevel && x == rhs.x && y == rhs.y; + } + + @Override + public int hashCode() { + int code = 17; + code *= 37 + zoomLevel; + code *= 37 + x; + code *= 37 + y; + return code; + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/MapTileCache.java b/src/main/java/org/osmdroid/tileprovider/MapTileCache.java new file mode 100644 index 000000000..57dfae244 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/MapTileCache.java @@ -0,0 +1,88 @@ +// Created by plusminus on 17:58:57 - 25.09.2008 +package org.osmdroid.tileprovider; + +import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; + +import android.graphics.drawable.Drawable; + +/** + * + * @author Nicolas Gramlich + * + */ +public class MapTileCache implements OpenStreetMapTileProviderConstants { + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected final Object mCachedTilesLockObject = new Object(); + protected LRUMapTileCache mCachedTiles; + + // =========================================================== + // Constructors + // =========================================================== + + public MapTileCache() { + this(CACHE_MAPTILECOUNT_DEFAULT); + } + + /** + * @param aMaximumCacheSize + * Maximum amount of MapTiles to be hold within. + */ + public MapTileCache(final int aMaximumCacheSize) { + this.mCachedTiles = new LRUMapTileCache(aMaximumCacheSize); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public void ensureCapacity(final int aCapacity) { + synchronized (mCachedTilesLockObject) { + mCachedTiles.ensureCapacity(aCapacity); + } + } + + public Drawable getMapTile(final MapTile aTile) { + synchronized (mCachedTilesLockObject) { + return this.mCachedTiles.get(aTile); + } + } + + public void putTile(final MapTile aTile, final Drawable aDrawable) { + if (aDrawable != null) { + synchronized (mCachedTilesLockObject) { + this.mCachedTiles.put(aTile, aDrawable); + } + } + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + public boolean containsTile(final MapTile aTile) { + synchronized (mCachedTilesLockObject) { + return this.mCachedTiles.containsKey(aTile); + } + } + + public void clear() { + synchronized (mCachedTilesLockObject) { + this.mCachedTiles.clear(); + } + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/tileprovider/MapTileProviderArray.java b/src/main/java/org/osmdroid/tileprovider/MapTileProviderArray.java new file mode 100644 index 000000000..f41259e12 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/MapTileProviderArray.java @@ -0,0 +1,236 @@ +package org.osmdroid.tileprovider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.graphics.drawable.Drawable; + +/** + * This top-level tile provider allows a consumer to provide an array of modular asynchronous tile + * providers to be used to obtain map tiles. When a tile is requested, the + * {@link MapTileProviderArray} first checks the {@link MapTileCache} (synchronously) and returns + * the tile if available. If not, then the {@link MapTileProviderArray} returns null and sends the + * tile request through the asynchronous tile request chain. Each asynchronous tile provider returns + * success/failure to the {@link MapTileProviderArray}. If successful, the + * {@link MapTileProviderArray} passes the result to the base class. If failed, then the next + * asynchronous tile provider is called in the chain. If there are no more asynchronous tile + * providers in the chain, then the failure result is passed to the base class. The + * {@link MapTileProviderArray} provides a mechanism so that only one unique tile-request can be in + * the map tile request chain at a time. + * + * @author Marc Kurtz + * + */ +public class MapTileProviderArray extends MapTileProviderBase { + + protected final HashMap mWorking; + + private static final Logger logger = LoggerFactory.getLogger(MapTileProviderArray.class); + + protected final List mTileProviderList; + + /** + * Creates an {@link MapTileProviderArray} with no tile providers. + * + * @param pRegisterReceiver + * a {@link IRegisterReceiver} + */ + protected MapTileProviderArray(final ITileSource pTileSource, + final IRegisterReceiver pRegisterReceiver) { + this(pTileSource, pRegisterReceiver, new MapTileModuleProviderBase[0]); + } + + /** + * Creates an {@link MapTileProviderArray} with the specified tile providers. + * + * @param aRegisterReceiver + * a {@link IRegisterReceiver} + * @param pTileProviderArray + * an array of {@link MapTileModuleProviderBase} + */ + public MapTileProviderArray(final ITileSource pTileSource, + final IRegisterReceiver aRegisterReceiver, + final MapTileModuleProviderBase[] pTileProviderArray) { + super(pTileSource); + + mWorking = new HashMap(); + + mTileProviderList = new ArrayList(); + Collections.addAll(mTileProviderList, pTileProviderArray); + } + + @Override + public void detach() { + synchronized (mTileProviderList) { + for (final MapTileModuleProviderBase tileProvider : mTileProviderList) { + tileProvider.detach(); + } + } + + synchronized (mWorking) { + mWorking.clear(); + } + } + + @Override + public Drawable getMapTile(final MapTile pTile) { + final Drawable tile = mTileCache.getMapTile(pTile); + if (tile != null && !ExpirableBitmapDrawable.isDrawableExpired(tile)) { + return tile; + } else { + boolean alreadyInProgress = false; + synchronized (mWorking) { + alreadyInProgress = mWorking.containsKey(pTile); + } + + if (!alreadyInProgress) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("MapTileProviderArray.getMapTile() requested but not in cache, trying from async providers: " + + pTile); + } + + final MapTileRequestState state; + synchronized (mTileProviderList) { + final MapTileModuleProviderBase[] providerArray = + new MapTileModuleProviderBase[mTileProviderList.size()]; + state = new MapTileRequestState(pTile, + mTileProviderList.toArray(providerArray), this); + } + + synchronized (mWorking) { + // Check again + alreadyInProgress = mWorking.containsKey(pTile); + if (alreadyInProgress) { + return null; + } + + mWorking.put(pTile, state); + } + + final MapTileModuleProviderBase provider = findNextAppropriateProvider(state); + if (provider != null) { + provider.loadMapTileAsync(state); + } else { + mapTileRequestFailed(state); + } + } + return tile; + } + } + + @Override + public void mapTileRequestCompleted(final MapTileRequestState aState, final Drawable aDrawable) { + synchronized (mWorking) { + mWorking.remove(aState.getMapTile()); + } + super.mapTileRequestCompleted(aState, aDrawable); + } + + @Override + public void mapTileRequestFailed(final MapTileRequestState aState) { + final MapTileModuleProviderBase nextProvider = findNextAppropriateProvider(aState); + if (nextProvider != null) { + nextProvider.loadMapTileAsync(aState); + } else { + synchronized (mWorking) { + mWorking.remove(aState.getMapTile()); + } + super.mapTileRequestFailed(aState); + } + } + + @Override + public void mapTileRequestExpiredTile(MapTileRequestState aState, Drawable aDrawable) { + // Call through to the super first so aState.getCurrentProvider() still contains the proper + // provider. + super.mapTileRequestExpiredTile(aState, aDrawable); + + // Continue through the provider chain + final MapTileModuleProviderBase nextProvider = findNextAppropriateProvider(aState); + if (nextProvider != null) { + nextProvider.loadMapTileAsync(aState); + } else { + synchronized (mWorking) { + mWorking.remove(aState.getMapTile()); + } + } + } + + /** + * We want to not use a provider that doesn't exist anymore in the chain, and we want to not use + * a provider that requires a data connection when one is not available. + */ + protected MapTileModuleProviderBase findNextAppropriateProvider(final MapTileRequestState aState) { + MapTileModuleProviderBase provider = null; + boolean providerDoesntExist = false, providerCantGetDataConnection = false, providerCantServiceZoomlevel = false; + // The logic of the while statement is + // "Keep looping until you get null, or a provider that still exists + // and has a data connection if it needs one and can service the zoom level," + do { + provider = aState.getNextProvider(); + // Perform some checks to see if we can use this provider + // If any of these are true, then that disqualifies the provider for this tile request. + if (provider != null) { + providerDoesntExist = !this.getProviderExists(provider); + providerCantGetDataConnection = !useDataConnection() + && provider.getUsesDataConnection(); + int zoomLevel = aState.getMapTile().getZoomLevel(); + providerCantServiceZoomlevel = zoomLevel > provider.getMaximumZoomLevel() + || zoomLevel < provider.getMinimumZoomLevel(); + } + } while ((provider != null) + && (providerDoesntExist || providerCantGetDataConnection || providerCantServiceZoomlevel)); + return provider; + } + + public boolean getProviderExists(final MapTileModuleProviderBase provider) { + synchronized (mTileProviderList) { + return mTileProviderList.contains(provider); + } + } + + @Override + public int getMinimumZoomLevel() { + int result = MAXIMUM_ZOOMLEVEL; + synchronized (mTileProviderList) { + for (final MapTileModuleProviderBase tileProvider : mTileProviderList) { + if (tileProvider.getMinimumZoomLevel() < result) { + result = tileProvider.getMinimumZoomLevel(); + } + } + } + return result; + } + + @Override + public int getMaximumZoomLevel() { + int result = MINIMUM_ZOOMLEVEL; + synchronized (mTileProviderList) { + for (final MapTileModuleProviderBase tileProvider : mTileProviderList) { + if (tileProvider.getMaximumZoomLevel() > result) { + result = tileProvider.getMaximumZoomLevel(); + } + } + } + return result; + } + + @Override + public void setTileSource(final ITileSource aTileSource) { + super.setTileSource(aTileSource); + + synchronized (mTileProviderList) { + for (final MapTileModuleProviderBase tileProvider : mTileProviderList) { + tileProvider.setTileSource(aTileSource); + clearTileCache(); + } + } + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/MapTileProviderBase.java b/src/main/java/org/osmdroid/tileprovider/MapTileProviderBase.java new file mode 100644 index 000000000..1ed4bcaa0 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/MapTileProviderBase.java @@ -0,0 +1,426 @@ +// Created by plusminus on 21:46:22 - 25.09.2008 +package org.osmdroid.tileprovider; + +import java.util.HashMap; + +import microsoft.mappoint.TileSystem; + +import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; +import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.util.TileLooper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; + +/** + * This is an abstract class. The tile provider is responsible for: + *
    + *
  • determining if a map tile is available,
  • + *
  • notifying the client, via a callback handler
  • + *
+ * see {@link MapTile} for an overview of how tiles are served by this provider. + * + * @author Marc Kurtz + * @author Nicolas Gramlich + * + */ +public abstract class MapTileProviderBase implements IMapTileProviderCallback, + OpenStreetMapTileProviderConstants { + + private static final Logger logger = LoggerFactory.getLogger(MapTileProviderBase.class); + + protected final MapTileCache mTileCache; + protected Handler mTileRequestCompleteHandler; + protected boolean mUseDataConnection = true; + + private ITileSource mTileSource; + + /** + * Attempts to get a Drawable that represents a {@link MapTile}. If the tile is not immediately + * available this will return null and attempt to get the tile from known tile sources for + * subsequent future requests. Note that this may return a {@link ReusableBitmapDrawable} in + * which case you should follow proper handling procedures for using that Drawable or it may + * reused while you are working with it. + * + * @see ReusableBitmapDrawable + */ + public abstract Drawable getMapTile(MapTile pTile); + + public abstract void detach(); + + /** + * Gets the minimum zoom level this tile provider can provide + * + * @return the minimum zoom level + */ + public abstract int getMinimumZoomLevel(); + + /** + * Gets the maximum zoom level this tile provider can provide + * + * @return the maximum zoom level + */ + public abstract int getMaximumZoomLevel(); + + /** + * Sets the tile source for this tile provider. + * + * @param pTileSource + * the tile source + */ + public void setTileSource(final ITileSource pTileSource) { + mTileSource = pTileSource; + clearTileCache(); + } + + /** + * Gets the tile source for this tile provider. + * + * @return the tile source + */ + public ITileSource getTileSource() { + return mTileSource; + } + + /** + * Creates a {@link MapTileCache} to be used to cache tiles in memory. + */ + public MapTileCache createTileCache() { + return new MapTileCache(); + } + + public MapTileProviderBase(final ITileSource pTileSource) { + this(pTileSource, null); + } + + public MapTileProviderBase(final ITileSource pTileSource, + final Handler pDownloadFinishedListener) { + mTileCache = this.createTileCache(); + mTileRequestCompleteHandler = pDownloadFinishedListener; + mTileSource = pTileSource; + } + + /** + * Called by implementation class methods indicating that they have completed the request as + * best it can. The tile is added to the cache, and a MAPTILE_SUCCESS_ID message is sent. + * + * @param pState + * the map tile request state object + * @param pDrawable + * the Drawable of the map tile + */ + @Override + public void mapTileRequestCompleted(final MapTileRequestState pState, final Drawable pDrawable) { + // put the tile in the cache + putTileIntoCache(pState, pDrawable); + + // tell our caller we've finished and it should update its view + if (mTileRequestCompleteHandler != null) { + mTileRequestCompleteHandler.sendEmptyMessage(MapTile.MAPTILE_SUCCESS_ID); + } + + if (DEBUG_TILE_PROVIDERS) { + logger.debug("MapTileProviderBase.mapTileRequestCompleted(): " + pState.getMapTile()); + } + } + + /** + * Called by implementation class methods indicating that they have failed to retrieve the + * requested map tile. a MAPTILE_FAIL_ID message is sent. + * + * @param pState + * the map tile request state object + */ + @Override + public void mapTileRequestFailed(final MapTileRequestState pState) { + if (mTileRequestCompleteHandler != null) { + mTileRequestCompleteHandler.sendEmptyMessage(MapTile.MAPTILE_FAIL_ID); + } + + if (DEBUG_TILE_PROVIDERS) { + logger.debug("MapTileProviderBase.mapTileRequestFailed(): " + pState.getMapTile()); + } + } + + /** + * Called by implementation class methods indicating that they have produced an expired result + * that can be used but better results may be delivered later. The tile is added to the cache, + * and a MAPTILE_SUCCESS_ID message is sent. + * + * @param pState + * the map tile request state object + * @param pDrawable + * the Drawable of the map tile + */ + @Override + public void mapTileRequestExpiredTile(MapTileRequestState pState, Drawable pDrawable) { + // Put the expired tile into the cache + putExpiredTileIntoCache(pState, pDrawable); + + // tell our caller we've finished and it should update its view + if (mTileRequestCompleteHandler != null) { + mTileRequestCompleteHandler.sendEmptyMessage(MapTile.MAPTILE_SUCCESS_ID); + } + + if (DEBUG_TILE_PROVIDERS) { + logger.debug("MapTileProviderBase.mapTileRequestExpiredTile(): " + pState.getMapTile()); + } + } + + protected void putTileIntoCache(MapTileRequestState pState, Drawable pDrawable) { + final MapTile tile = pState.getMapTile(); + if (pDrawable != null) { + mTileCache.putTile(tile, pDrawable); + } + } + + protected void putExpiredTileIntoCache(MapTileRequestState pState, Drawable pDrawable) { + final MapTile tile = pState.getMapTile(); + if (pDrawable != null && !mTileCache.containsTile(tile)) { + mTileCache.putTile(tile, pDrawable); + } + } + + public void setTileRequestCompleteHandler(final Handler handler) { + mTileRequestCompleteHandler = handler; + } + + public void ensureCapacity(final int pCapacity) { + mTileCache.ensureCapacity(pCapacity); + } + + public void clearTileCache() { + mTileCache.clear(); + } + + /** + * Whether to use the network connection if it's available. + */ + @Override + public boolean useDataConnection() { + return mUseDataConnection; + } + + /** + * Set whether to use the network connection if it's available. + * + * @param pMode + * if true use the network connection if it's available. if false don't use the + * network connection even if it's available. + */ + public void setUseDataConnection(final boolean pMode) { + mUseDataConnection = pMode; + } + + /** + * Recreate the cache using scaled versions of the tiles currently in it + * @param pNewZoomLevel the zoom level that we need now + * @param pOldZoomLevel the previous zoom level that we should get the tiles to rescale + * @param pViewPort the view port we need tiles for + */ + public void rescaleCache(final int pNewZoomLevel, final int pOldZoomLevel, final Rect pViewPort) { + + if (pNewZoomLevel == pOldZoomLevel) { + return; + } + + final long startMs = System.currentTimeMillis(); + + logger.info("rescale tile cache from "+ pOldZoomLevel + " to " + pNewZoomLevel); + + final int tileSize = getTileSource().getTileSizePixels(); + final int worldSize_2 = TileSystem.MapSize(pNewZoomLevel) >> 1; + final Rect viewPort = new Rect(pViewPort); + viewPort.offset(worldSize_2, worldSize_2); + + final ScaleTileLooper tileLooper = pNewZoomLevel > pOldZoomLevel + ? new ZoomInTileLooper(pOldZoomLevel) + : new ZoomOutTileLooper(pOldZoomLevel); + tileLooper.loop(null, pNewZoomLevel, tileSize, viewPort); + + final long endMs = System.currentTimeMillis(); + logger.info("Finished rescale in " + (endMs - startMs) + "ms"); + } + + private abstract class ScaleTileLooper extends TileLooper { + + /** new (scaled) tiles to add to cache + * NB first generate all and then put all in cache, + * otherwise the ones we need will be pushed out */ + protected final HashMap mNewTiles; + + protected final int mOldZoomLevel; + protected int mDiff; + protected int mTileSize_2; + protected Rect mSrcRect; + protected Rect mDestRect; + protected Paint mDebugPaint; + + public ScaleTileLooper(final int pOldZoomLevel) { + mOldZoomLevel = pOldZoomLevel; + mNewTiles = new HashMap(); + mSrcRect = new Rect(); + mDestRect = new Rect(); + mDebugPaint = new Paint(); + } + + @Override + public void initialiseLoop(final int pZoomLevel, final int pTileSizePx) { + mDiff = Math.abs(pZoomLevel - mOldZoomLevel); + mTileSize_2 = pTileSizePx >> mDiff; + } + + @Override + public void handleTile(final Canvas pCanvas, final int pTileSizePx, final MapTile pTile, final int pX, final int pY) { + + // Get tile from cache. + // If it's found then no need to created scaled version. + // If not found (null) them we've initiated a new request for it, + // and now we'll create a scaled version until the request completes. + final Drawable requestedTile = getMapTile(pTile); + if (requestedTile == null) { + try { + handleTile(pTileSizePx, pTile, pX, pY); + } catch(final OutOfMemoryError e) { + logger.error("OutOfMemoryError rescaling cache"); + } + } + } + + @Override + public void finaliseLoop() { + // now add the new ones, pushing out the old ones + while (!mNewTiles.isEmpty()) { + final MapTile tile = mNewTiles.keySet().iterator().next(); + final Bitmap bitmap = mNewTiles.remove(tile); + final ExpirableBitmapDrawable drawable = new ReusableBitmapDrawable(bitmap); + drawable.setState(new int[] { ExpirableBitmapDrawable.EXPIRED }); + Drawable existingTile = mTileCache.getMapTile(tile); + if (existingTile == null || ExpirableBitmapDrawable.isDrawableExpired(existingTile)) + putExpiredTileIntoCache(new MapTileRequestState(tile, + new MapTileModuleProviderBase[0], null), drawable); + } + } + + protected abstract void handleTile(int pTileSizePx, MapTile pTile, int pX, int pY); + } + + private class ZoomInTileLooper extends ScaleTileLooper { + public ZoomInTileLooper(final int pOldZoomLevel) { + super(pOldZoomLevel); + } + @Override + public void handleTile(final int pTileSizePx, final MapTile pTile, final int pX, final int pY) { + // get the correct fraction of the tile from cache and scale up + + final MapTile oldTile = new MapTile(mOldZoomLevel, pTile.getX() >> mDiff, pTile.getY() >> mDiff); + final Drawable oldDrawable = mTileCache.getMapTile(oldTile); + + if (oldDrawable instanceof BitmapDrawable) { + final int xx = (pX % (1 << mDiff)) * mTileSize_2; + final int yy = (pY % (1 << mDiff)) * mTileSize_2; + mSrcRect.set(xx, yy, xx + mTileSize_2, yy + mTileSize_2); + mDestRect.set(0, 0, pTileSizePx, pTileSizePx); + + // Try to get a bitmap from the pool, otherwise allocate a new one + Bitmap bitmap; + bitmap = BitmapPool.getInstance().obtainSizedBitmapFromPool(pTileSizePx, + pTileSizePx); + if (bitmap == null) + bitmap = Bitmap.createBitmap(pTileSizePx, pTileSizePx, + Bitmap.Config.ARGB_8888); + + final Canvas canvas = new Canvas(bitmap); + final boolean isReusable = oldDrawable instanceof ReusableBitmapDrawable; + boolean success = false; + if (isReusable) + ((ReusableBitmapDrawable) oldDrawable).beginUsingDrawable(); + try { + if (!isReusable || ((ReusableBitmapDrawable) oldDrawable).isBitmapValid()) { + final Bitmap oldBitmap = ((BitmapDrawable) oldDrawable).getBitmap(); + canvas.drawBitmap(oldBitmap, mSrcRect, mDestRect, null); + success = true; + if (DEBUGMODE) { + logger.debug("Created scaled tile: " + pTile); + mDebugPaint.setTextSize(40); + canvas.drawText("scaled", 50, 50, mDebugPaint); + } + } + } finally { + if (isReusable) + ((ReusableBitmapDrawable) oldDrawable).finishUsingDrawable(); + } + if (success) + mNewTiles.put(pTile, bitmap); + } + } + } + + private class ZoomOutTileLooper extends ScaleTileLooper { + private static final int MAX_ZOOM_OUT_DIFF = 4; + public ZoomOutTileLooper(final int pOldZoomLevel) { + super(pOldZoomLevel); + } + @Override + protected void handleTile(final int pTileSizePx, final MapTile pTile, final int pX, final int pY) { + + if (mDiff >= MAX_ZOOM_OUT_DIFF){ + return; + } + + // get many tiles from cache and make one tile from them + final int xx = pTile.getX() << mDiff; + final int yy = pTile.getY() << mDiff; + final int numTiles = 1 << mDiff; + Bitmap bitmap = null; + Canvas canvas = null; + for(int x = 0; x < numTiles; x++) { + for(int y = 0; y < numTiles; y++) { + final MapTile oldTile = new MapTile(mOldZoomLevel, xx + x, yy + y); + final Drawable oldDrawable = mTileCache.getMapTile(oldTile); + if (oldDrawable instanceof BitmapDrawable) { + final Bitmap oldBitmap = ((BitmapDrawable)oldDrawable).getBitmap(); + if (oldBitmap != null) { + if (bitmap == null) { + // Try to get a bitmap from the pool, otherwise allocate a new one + bitmap = BitmapPool.getInstance().obtainSizedBitmapFromPool( + pTileSizePx, pTileSizePx); + if (bitmap == null) + bitmap = Bitmap.createBitmap(pTileSizePx, pTileSizePx, + Bitmap.Config.ARGB_8888); + canvas = new Canvas(bitmap); + canvas.drawColor(Color.LTGRAY); + } + mDestRect.set( + x * mTileSize_2, y * mTileSize_2, + (x + 1) * mTileSize_2, (y + 1) * mTileSize_2); + if (oldBitmap != null) { + canvas.drawBitmap(oldBitmap, null, mDestRect, null); + mTileCache.mCachedTiles.remove(oldBitmap); + } + } + } + } + } + + if (bitmap != null) { + mNewTiles.put(pTile, bitmap); + if (DEBUGMODE) { + logger.debug("Created scaled tile: " + pTile); + mDebugPaint.setTextSize(40); + canvas.drawText("scaled", 50, 50, mDebugPaint); + } + } + } + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/MapTileProviderBasic.java b/src/main/java/org/osmdroid/tileprovider/MapTileProviderBasic.java new file mode 100644 index 000000000..944b117be --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/MapTileProviderBasic.java @@ -0,0 +1,63 @@ +package org.osmdroid.tileprovider; + +import org.osmdroid.tileprovider.modules.INetworkAvailablityCheck; +import org.osmdroid.tileprovider.modules.MapTileDownloader; +import org.osmdroid.tileprovider.modules.MapTileFileArchiveProvider; +import org.osmdroid.tileprovider.modules.MapTileFilesystemProvider; +import org.osmdroid.tileprovider.modules.NetworkAvailabliltyCheck; +import org.osmdroid.tileprovider.modules.TileWriter; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.tileprovider.util.SimpleRegisterReceiver; + +import android.content.Context; + +/** + * This top-level tile provider implements a basic tile request chain which includes a + * {@link MapTileFilesystemProvider} (a file-system cache), a {@link MapTileFileArchiveProvider} + * (archive provider), and a {@link MapTileDownloader} (downloads map tiles via tile source). + * + * @author Marc Kurtz + * + */ +public class MapTileProviderBasic extends MapTileProviderArray implements IMapTileProviderCallback { + + // private static final Logger logger = LoggerFactory.getLogger(MapTileProviderBasic.class); + + /** + * Creates a {@link MapTileProviderBasic}. + */ + public MapTileProviderBasic(final Context pContext) { + this(pContext, TileSourceFactory.DEFAULT_TILE_SOURCE); + } + + /** + * Creates a {@link MapTileProviderBasic}. + */ + public MapTileProviderBasic(final Context pContext, final ITileSource pTileSource) { + this(new SimpleRegisterReceiver(pContext), new NetworkAvailabliltyCheck(pContext), + pTileSource); + } + + /** + * Creates a {@link MapTileProviderBasic}. + */ + public MapTileProviderBasic(final IRegisterReceiver pRegisterReceiver, + final INetworkAvailablityCheck aNetworkAvailablityCheck, final ITileSource pTileSource) { + super(pTileSource, pRegisterReceiver); + + final TileWriter tileWriter = new TileWriter(); + + final MapTileFilesystemProvider fileSystemProvider = new MapTileFilesystemProvider( + pRegisterReceiver, pTileSource); + mTileProviderList.add(fileSystemProvider); + + final MapTileFileArchiveProvider archiveProvider = new MapTileFileArchiveProvider( + pRegisterReceiver, pTileSource); + mTileProviderList.add(archiveProvider); + + final MapTileDownloader downloaderProvider = new MapTileDownloader(pTileSource, tileWriter, + aNetworkAvailablityCheck); + mTileProviderList.add(downloaderProvider); + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/MapTileRequestState.java b/src/main/java/org/osmdroid/tileprovider/MapTileRequestState.java new file mode 100644 index 000000000..8a4279798 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/MapTileRequestState.java @@ -0,0 +1,45 @@ +package org.osmdroid.tileprovider; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.Queue; + +import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase; + +public class MapTileRequestState { + + private final Queue mProviderQueue; + private final MapTile mMapTile; + private final IMapTileProviderCallback mCallback; + private MapTileModuleProviderBase mCurrentProvider; + + public MapTileRequestState(final MapTile mapTile, + final MapTileModuleProviderBase[] providers, + final IMapTileProviderCallback callback) { + mProviderQueue = new LinkedList(); + Collections.addAll(mProviderQueue, providers); + mMapTile = mapTile; + mCallback = callback; + } + + public MapTile getMapTile() { + return mMapTile; + } + + public IMapTileProviderCallback getCallback() { + return mCallback; + } + + public boolean isEmpty() { + return mProviderQueue.isEmpty(); + } + + public MapTileModuleProviderBase getNextProvider() { + mCurrentProvider = mProviderQueue.poll(); + return mCurrentProvider; + } + + public MapTileModuleProviderBase getCurrentProvider() { + return mCurrentProvider; + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/ReusableBitmapDrawable.java b/src/main/java/org/osmdroid/tileprovider/ReusableBitmapDrawable.java new file mode 100644 index 000000000..229a141ef --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/ReusableBitmapDrawable.java @@ -0,0 +1,53 @@ +package org.osmdroid.tileprovider; + +import android.graphics.Bitmap; + +/** + * A {@link ExpirableBitmapDrawable} class that allows keeping track of usage references. This + * facilitates the ability to reuse the underlying Bitmaps if no references are active. To safely + * use the Drawable first call {@link #beginUsingDrawable()} and then check {@link #isBitmapValid()} + * to ensure that the Drawable is still valid. When done using the Drawable you must call + * {@link #finishUsingDrawable()} to release the reference and allow the Bitmap to be reused later. + * + * @author Marc Kurtz + * + */ +public class ReusableBitmapDrawable extends ExpirableBitmapDrawable { + + private boolean mBitmapRecycled = false; + private int mUsageRefCount = 0; + + public ReusableBitmapDrawable(Bitmap pBitmap) { + super(pBitmap); + } + + public void beginUsingDrawable() { + synchronized (this) { + mUsageRefCount++; + } + } + + public void finishUsingDrawable() { + synchronized (this) { + mUsageRefCount--; + if (mUsageRefCount < 0) + throw new IllegalStateException("Unbalanced endUsingDrawable() called."); + } + } + + public Bitmap tryRecycle() { + synchronized (this) { + if (mUsageRefCount == 0) { + mBitmapRecycled = true; + return getBitmap(); + } + } + return null; + } + + public boolean isBitmapValid() { + synchronized (this) { + return !mBitmapRecycled; + } + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/constants/OpenStreetMapTileProviderConstants.java b/src/main/java/org/osmdroid/tileprovider/constants/OpenStreetMapTileProviderConstants.java new file mode 100644 index 000000000..32cca00ec --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/constants/OpenStreetMapTileProviderConstants.java @@ -0,0 +1,72 @@ +package org.osmdroid.tileprovider.constants; + +import java.io.File; + +import android.os.Environment; + +/** + * + * This class contains constants used by the tile provider. + * + * @author Neil Boyd + * + */ +public interface OpenStreetMapTileProviderConstants { + + public static final boolean DEBUGMODE = false; + public static final boolean DEBUG_TILE_PROVIDERS = false; + + /** Minimum Zoom Level */ + public static final int MINIMUM_ZOOMLEVEL = 0; + + /** + * Maximum Zoom Level - we use Integers to store zoom levels so overflow happens at 2^32 - 1, + * but we also have a tile size that is typically 2^8, so (32-1)-8-1 = 22 + */ + public static final int MAXIMUM_ZOOMLEVEL = 22; + + /** Base path for osmdroid files. Zip files are in this folder. */ + public static final File OSMDROID_PATH = new File(Environment.getExternalStorageDirectory(), + "osmdroid"); + + /** Base path for tiles. */ + public static final File TILE_PATH_BASE = new File(OSMDROID_PATH, "tiles"); + + /** add an extension to files on sdcard so that gallery doesn't index them */ + public static final String TILE_PATH_EXTENSION = ".tile"; + + /** + * Initial tile cache size. The size will be increased as required by calling {@link + * LRUMapTileCache.ensureCapacity(int)} The tile cache will always be at least 3x3. + */ + public static final int CACHE_MAPTILECOUNT_DEFAULT = 9; + + /** + * number of tile download threads, conforming to OSM policy: + * http://wiki.openstreetmap.org/wiki/Tile_usage_policy + */ + public static final int NUMBER_OF_TILE_DOWNLOAD_THREADS = 2; + + public static final int NUMBER_OF_TILE_FILESYSTEM_THREADS = 8; + + public static final long ONE_SECOND = 1000; + public static final long ONE_MINUTE = ONE_SECOND * 60; + public static final long ONE_HOUR = ONE_MINUTE * 60; + public static final long ONE_DAY = ONE_HOUR * 24; + public static final long ONE_WEEK = ONE_DAY * 7; + public static final long ONE_YEAR = ONE_DAY * 365; + public static final long DEFAULT_MAXIMUM_CACHED_FILE_AGE = ONE_WEEK; + + public static final int TILE_DOWNLOAD_MAXIMUM_QUEUE_SIZE = 40; + public static final int TILE_FILESYSTEM_MAXIMUM_QUEUE_SIZE = 40; + + /** 30 days */ + public static final long TILE_EXPIRY_TIME_MILLISECONDS = 1000L * 60 * 60 * 24 * 30; + + /** 600 Mb */ + public static final long TILE_MAX_CACHE_SIZE_BYTES = 600L * 1024 * 1024; + + /** 500 Mb */ + public static final long TILE_TRIM_CACHE_SIZE_BYTES = 500L * 1024 * 1024; + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/ArchiveFileFactory.java b/src/main/java/org/osmdroid/tileprovider/modules/ArchiveFileFactory.java new file mode 100644 index 000000000..cecd54594 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/ArchiveFileFactory.java @@ -0,0 +1,56 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.database.sqlite.SQLiteException; + +public class ArchiveFileFactory { + + private static final Logger logger = LoggerFactory.getLogger(ArchiveFileFactory.class); + + /** + * Return an implementation of {@link IArchiveFile} for the specified file. + * @return an implementation, or null if there's no suitable implementation + */ + public static IArchiveFile getArchiveFile(final File pFile) { + + if (pFile.getName().endsWith(".zip")) { + try { + return ZipFileArchive.getZipFileArchive(pFile); + } catch (final IOException e) { + logger.error("Error opening ZIP file", e); + } + } + + if (pFile.getName().endsWith(".sqlite")) { + try { + return DatabaseFileArchive.getDatabaseFileArchive(pFile); + } catch (final SQLiteException e) { + logger.error("Error opening SQL file", e); + } + } + + if (pFile.getName().endsWith(".mbtiles")) { + try { + return MBTilesFileArchive.getDatabaseFileArchive(pFile); + } catch (final SQLiteException e) { + logger.error("Error opening MBTiles SQLite file", e); + } + } + + if (pFile.getName().endsWith(".gemf")) { + try { + return GEMFFileArchive.getGEMFFileArchive(pFile); + } catch (final IOException e) { + logger.error("Error opening GEMF file", e); + } + } + + return null; + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/ConfigurablePriorityThreadFactory.java b/src/main/java/org/osmdroid/tileprovider/modules/ConfigurablePriorityThreadFactory.java new file mode 100644 index 000000000..7522a2767 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/ConfigurablePriorityThreadFactory.java @@ -0,0 +1,30 @@ +package org.osmdroid.tileprovider.modules; + +import java.util.concurrent.ThreadFactory; + +/** + * + * @author Jastrzab + */ + +public class ConfigurablePriorityThreadFactory implements ThreadFactory { + + private final int mPriority; + private final String mName; + + public ConfigurablePriorityThreadFactory(final int pPriority, final String pName) { + mPriority = pPriority; + mName = pName; + } + + @Override + public Thread newThread(final Runnable pRunnable) { + final Thread thread = new Thread(pRunnable); + thread.setPriority(mPriority); + if (mName != null) { + thread.setName(mName); + } + return thread; + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/DatabaseFileArchive.java b/src/main/java/org/osmdroid/tileprovider/modules/DatabaseFileArchive.java new file mode 100644 index 000000000..441647222 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/DatabaseFileArchive.java @@ -0,0 +1,60 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; + +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +public class DatabaseFileArchive implements IArchiveFile { + + private static final Logger logger = LoggerFactory.getLogger(DatabaseFileArchive.class); + + private final SQLiteDatabase mDatabase; + + private DatabaseFileArchive(final SQLiteDatabase pDatabase) { + mDatabase = pDatabase; + } + + public static DatabaseFileArchive getDatabaseFileArchive(final File pFile) throws SQLiteException { + return new DatabaseFileArchive(SQLiteDatabase.openOrCreateDatabase(pFile, null)); + } + + @Override + public InputStream getInputStream(final ITileSource pTileSource, final MapTile pTile) { + try { + InputStream ret = null; + final String[] tile = {"tile"}; + final long x = (long) pTile.getX(); + final long y = (long) pTile.getY(); + final long z = (long) pTile.getZoomLevel(); + final long index = ((z << z) + x << z) + y; + final Cursor cur = mDatabase.query("tiles", tile, "key = " + index + " and provider = '" + pTileSource.name() + "'", null, null, null, null); + if(cur.getCount() != 0) { + cur.moveToFirst(); + ret = new ByteArrayInputStream(cur.getBlob(0)); + } + cur.close(); + if(ret != null) { + return ret; + } + } catch(final Throwable e) { + logger.warn("Error getting db stream: " + pTile, e); + } + + return null; + } + + @Override + public String toString() { + return "DatabaseFileArchive [mDatabase=" + mDatabase.getPath() + "]"; + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/GEMFFileArchive.java b/src/main/java/org/osmdroid/tileprovider/modules/GEMFFileArchive.java new file mode 100644 index 000000000..de03feb20 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/GEMFFileArchive.java @@ -0,0 +1,34 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.util.GEMFFile; + +public class GEMFFileArchive implements IArchiveFile { + + private final GEMFFile mFile; + + private GEMFFileArchive(final File pFile) throws FileNotFoundException, IOException { + mFile = new GEMFFile(pFile); + } + + public static GEMFFileArchive getGEMFFileArchive(final File pFile) throws FileNotFoundException, IOException { + return new GEMFFileArchive(pFile); + } + + @Override + public InputStream getInputStream(final ITileSource pTileSource, final MapTile pTile) { + return mFile.getInputStream(pTile.getX(), pTile.getY(), pTile.getZoomLevel()); + } + + @Override + public String toString() { + return "GEMFFileArchive [mGEMFFile=" + mFile.getName() + "]"; + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/IArchiveFile.java b/src/main/java/org/osmdroid/tileprovider/modules/IArchiveFile.java new file mode 100644 index 000000000..04ed93400 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/IArchiveFile.java @@ -0,0 +1,16 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.InputStream; + +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.tilesource.ITileSource; + +public interface IArchiveFile { + + /** + * Get the input stream for the requested tile. + * @return the input stream, or null if the archive doesn't contain an entry for the requested tile + */ + InputStream getInputStream(ITileSource tileSource, MapTile tile); + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/IFilesystemCache.java b/src/main/java/org/osmdroid/tileprovider/modules/IFilesystemCache.java new file mode 100644 index 000000000..942b886a8 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/IFilesystemCache.java @@ -0,0 +1,29 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.InputStream; + +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.tilesource.ITileSource; + +/** + * Represents a write-only interface into a file system cache. + * + * @author Marc Kurtz + * + */ +public interface IFilesystemCache { + /** + * Save an InputStream as the specified tile in the file system cache for the specified tile + * source. + * + * @param pTileSourceInfo + * a tile source + * @param pTile + * a tile + * @param pStream + * an InputStream + * @return + */ + boolean saveFile(final ITileSource pTileSourceInfo, MapTile pTile, + final InputStream pStream); +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/INetworkAvailablityCheck.java b/src/main/java/org/osmdroid/tileprovider/modules/INetworkAvailablityCheck.java new file mode 100644 index 000000000..740a7dffe --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/INetworkAvailablityCheck.java @@ -0,0 +1,12 @@ +package org.osmdroid.tileprovider.modules; + +public interface INetworkAvailablityCheck { + + boolean getNetworkAvailable(); + + boolean getWiFiNetworkAvailable(); + + boolean getCellularDataNetworkAvailable(); + + boolean getRouteToPathExists(int hostAddress); +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/MBTilesFileArchive.java b/src/main/java/org/osmdroid/tileprovider/modules/MBTilesFileArchive.java new file mode 100644 index 000000000..3bb0144e3 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/MBTilesFileArchive.java @@ -0,0 +1,74 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; + +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +public class MBTilesFileArchive implements IArchiveFile { + + private static final Logger logger = LoggerFactory.getLogger(MBTilesFileArchive.class); + + private final SQLiteDatabase mDatabase; + + // TABLE tiles (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_data BLOB); + public final static String TABLE_TILES = "tiles"; + public final static String COL_TILES_ZOOM_LEVEL = "zoom_level"; + public final static String COL_TILES_TILE_COLUMN = "tile_column"; + public final static String COL_TILES_TILE_ROW = "tile_row"; + public final static String COL_TILES_TILE_DATA = "tile_data"; + + private MBTilesFileArchive(final SQLiteDatabase pDatabase) { + mDatabase = pDatabase; + } + + public static MBTilesFileArchive getDatabaseFileArchive(final File pFile) throws SQLiteException { + return new MBTilesFileArchive( + SQLiteDatabase.openDatabase( + pFile.getAbsolutePath(), + null, + SQLiteDatabase.NO_LOCALIZED_COLLATORS | SQLiteDatabase.OPEN_READONLY)); + } + + @Override + public InputStream getInputStream(final ITileSource pTileSource, final MapTile pTile) { + try { + InputStream ret = null; + final String[] tile = { COL_TILES_TILE_DATA }; + final String[] xyz = { + Integer.toString(pTile.getX()) + , Double.toString(Math.pow(2, pTile.getZoomLevel()) - pTile.getY() - 1) // Use Google Tiling Spec + , Integer.toString(pTile.getZoomLevel()) + }; + + final Cursor cur = mDatabase.query(TABLE_TILES, tile, "tile_column=? and tile_row=? and zoom_level=?", xyz, null, null, null); + + if(cur.getCount() != 0) { + cur.moveToFirst(); + ret = new ByteArrayInputStream(cur.getBlob(0)); + } + cur.close(); + if(ret != null) { + return ret; + } + } catch(final Throwable e) { + logger.warn("Error getting db stream: " + pTile, e); + } + + return null; + } + + @Override + public String toString() { + return "DatabaseFileArchive [mDatabase=" + mDatabase.getPath() + "]"; + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/MapTileDownloader.java b/src/main/java/org/osmdroid/tileprovider/modules/MapTileDownloader.java new file mode 100644 index 000000000..8ef7542e4 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/MapTileDownloader.java @@ -0,0 +1,251 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.UnknownHostException; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.osmdroid.http.HttpClientFactory; +import org.osmdroid.tileprovider.BitmapPool; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.MapTileRequestState; +import org.osmdroid.tileprovider.ReusableBitmapDrawable; +import org.osmdroid.tileprovider.tilesource.BitmapTileSourceBase.LowMemoryException; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase; +import org.osmdroid.tileprovider.util.StreamUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.graphics.drawable.Drawable; +import android.text.TextUtils; + +/** + * The {@link MapTileDownloader} loads tiles from an HTTP server. It saves downloaded tiles to an + * IFilesystemCache if available. + * + * @author Marc Kurtz + * @author Nicolas Gramlich + * @author Manuel Stahl + * + */ +public class MapTileDownloader extends MapTileModuleProviderBase { + + // =========================================================== + // Constants + // =========================================================== + + private static final Logger logger = LoggerFactory.getLogger(MapTileDownloader.class); + + // =========================================================== + // Fields + // =========================================================== + + private final IFilesystemCache mFilesystemCache; + + private final AtomicReference mTileSource = new AtomicReference(); + + private final INetworkAvailablityCheck mNetworkAvailablityCheck; + + // =========================================================== + // Constructors + // =========================================================== + + public MapTileDownloader(final ITileSource pTileSource) { + this(pTileSource, null, null); + } + + public MapTileDownloader(final ITileSource pTileSource, final IFilesystemCache pFilesystemCache) { + this(pTileSource, pFilesystemCache, null); + } + + public MapTileDownloader(final ITileSource pTileSource, + final IFilesystemCache pFilesystemCache, + final INetworkAvailablityCheck pNetworkAvailablityCheck) { + this(pTileSource, pFilesystemCache, pNetworkAvailablityCheck, + NUMBER_OF_TILE_DOWNLOAD_THREADS, TILE_DOWNLOAD_MAXIMUM_QUEUE_SIZE); + } + + public MapTileDownloader(final ITileSource pTileSource, + final IFilesystemCache pFilesystemCache, + final INetworkAvailablityCheck pNetworkAvailablityCheck, int pThreadPoolSize, + int pPendingQueueSize) { + super(pThreadPoolSize, pPendingQueueSize); + + mFilesystemCache = pFilesystemCache; + mNetworkAvailablityCheck = pNetworkAvailablityCheck; + setTileSource(pTileSource); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public ITileSource getTileSource() { + return mTileSource.get(); + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public boolean getUsesDataConnection() { + return true; + } + + @Override + protected String getName() { + return "Online Tile Download Provider"; + } + + @Override + protected String getThreadGroupName() { + return "downloader"; + } + + @Override + protected Runnable getTileLoader() { + return new TileLoader(); + } + + @Override + public int getMinimumZoomLevel() { + OnlineTileSourceBase tileSource = mTileSource.get(); + return (tileSource != null ? tileSource.getMinimumZoomLevel() : MINIMUM_ZOOMLEVEL); + } + + @Override + public int getMaximumZoomLevel() { + OnlineTileSourceBase tileSource = mTileSource.get(); + return (tileSource != null ? tileSource.getMaximumZoomLevel() : MAXIMUM_ZOOMLEVEL); + } + + @Override + public void setTileSource(final ITileSource tileSource) { + // We are only interested in OnlineTileSourceBase tile sources + if (tileSource instanceof OnlineTileSourceBase) { + mTileSource.set((OnlineTileSourceBase) tileSource); + } else { + // Otherwise shut down the tile downloader + mTileSource.set(null); + } + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + protected class TileLoader extends MapTileModuleProviderBase.TileLoader { + + @Override + public Drawable loadTile(final MapTileRequestState aState) throws CantContinueException { + + OnlineTileSourceBase tileSource = mTileSource.get(); + if (tileSource == null) { + return null; + } + + InputStream in = null; + OutputStream out = null; + final MapTile tile = aState.getMapTile(); + + try { + + if (mNetworkAvailablityCheck != null + && !mNetworkAvailablityCheck.getNetworkAvailable()) { + if (DEBUGMODE) { + logger.debug("Skipping " + getName() + " due to NetworkAvailabliltyCheck."); + } + return null; + } + + final String tileURLString = tileSource.getTileURLString(tile); + + if (DEBUGMODE) { + logger.debug("Downloading Maptile from url: " + tileURLString); + } + + if (TextUtils.isEmpty(tileURLString)) { + return null; + } + + final HttpClient client = HttpClientFactory.createHttpClient(); + final HttpUriRequest head = new HttpGet(tileURLString); + final HttpResponse response = client.execute(head); + + // Check to see if we got success + final org.apache.http.StatusLine line = response.getStatusLine(); + if (line.getStatusCode() != 200) { + logger.warn("Problem downloading MapTile: " + tile + " HTTP response: " + line); + return null; + } + + final HttpEntity entity = response.getEntity(); + if (entity == null) { + logger.warn("No content downloading MapTile: " + tile); + return null; + } + in = entity.getContent(); + + final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); + out = new BufferedOutputStream(dataStream, StreamUtils.IO_BUFFER_SIZE); + StreamUtils.copy(in, out); + out.flush(); + final byte[] data = dataStream.toByteArray(); + final ByteArrayInputStream byteStream = new ByteArrayInputStream(data); + + // Save the data to the filesystem cache + if (mFilesystemCache != null) { + mFilesystemCache.saveFile(tileSource, tile, byteStream); + byteStream.reset(); + } + final Drawable result = tileSource.getDrawable(byteStream); + + return result; + } catch (final UnknownHostException e) { + // no network connection so empty the queue + logger.warn("UnknownHostException downloading MapTile: " + tile + " : " + e); + throw new CantContinueException(e); + } catch (final LowMemoryException e) { + // low memory so empty the queue + logger.warn("LowMemoryException downloading MapTile: " + tile + " : " + e); + throw new CantContinueException(e); + } catch (final FileNotFoundException e) { + logger.warn("Tile not found: " + tile + " : " + e); + } catch (final IOException e) { + logger.warn("IOException downloading MapTile: " + tile + " : " + e); + } catch (final Throwable e) { + logger.error("Error downloading MapTile: " + tile, e); + } finally { + StreamUtils.closeStream(in); + StreamUtils.closeStream(out); + } + + return null; + } + + @Override + protected void tileLoaded(final MapTileRequestState pState, final Drawable pDrawable) { + removeTileFromQueues(pState.getMapTile()); + // don't return the tile because we'll wait for the fs provider to ask for it + // this prevent flickering when a load of delayed downloads complete for tiles + // that we might not even be interested in any more + pState.getCallback().mapTileRequestCompleted(pState, null); + // We want to return the Bitmap to the BitmapPool if applicable + if (pDrawable instanceof ReusableBitmapDrawable) + BitmapPool.getInstance().returnDrawableToPool((ReusableBitmapDrawable) pDrawable); + } + + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/MapTileFileArchiveProvider.java b/src/main/java/org/osmdroid/tileprovider/modules/MapTileFileArchiveProvider.java new file mode 100644 index 000000000..b34ac3776 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/MapTileFileArchiveProvider.java @@ -0,0 +1,234 @@ +// Created by plusminus on 21:46:41 - 25.09.2008 +package org.osmdroid.tileprovider.modules; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; + +import org.osmdroid.tileprovider.IRegisterReceiver; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.MapTileProviderBase; +import org.osmdroid.tileprovider.MapTileRequestState; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.tileprovider.util.StreamUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.graphics.drawable.Drawable; + +/** + * A tile provider that can serve tiles from an archive using the supplied tile source. The tile + * provider will automatically find existing archives and use each one that it finds. + * + * @author Marc Kurtz + * @author Nicolas Gramlich + * + */ +public class MapTileFileArchiveProvider extends MapTileFileStorageProviderBase { + + // =========================================================== + // Constants + // =========================================================== + + private static final Logger logger = LoggerFactory.getLogger(MapTileFileArchiveProvider.class); + + // =========================================================== + // Fields + // =========================================================== + + private final ArrayList mArchiveFiles = new ArrayList(); + + private final AtomicReference mTileSource = new AtomicReference(); + + /** Disable the search of archives if specified in constructor */ + private final boolean mSpecificArchivesProvided; + + // =========================================================== + // Constructors + // =========================================================== + + /** + * The tiles may be found on several media. This one works with tiles stored on the file system. + * It and its friends are typically created and controlled by {@link MapTileProviderBase}. + */ + public MapTileFileArchiveProvider(final IRegisterReceiver pRegisterReceiver, + final ITileSource pTileSource, final IArchiveFile[] pArchives) { + super(pRegisterReceiver, NUMBER_OF_TILE_FILESYSTEM_THREADS, + TILE_FILESYSTEM_MAXIMUM_QUEUE_SIZE); + + setTileSource(pTileSource); + + if (pArchives == null) { + mSpecificArchivesProvided = false; + findArchiveFiles(); + } else { + mSpecificArchivesProvided = true; + for (int i = pArchives.length - 1; i >= 0; i--) { + mArchiveFiles.add(pArchives[i]); + } + } + + } + + public MapTileFileArchiveProvider(final IRegisterReceiver pRegisterReceiver, + final ITileSource pTileSource) { + this(pRegisterReceiver, pTileSource, null); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public boolean getUsesDataConnection() { + return false; + } + + @Override + protected String getName() { + return "File Archive Provider"; + } + + @Override + protected String getThreadGroupName() { + return "filearchive"; + } + + @Override + protected Runnable getTileLoader() { + return new TileLoader(); + } + + @Override + public int getMinimumZoomLevel() { + ITileSource tileSource = mTileSource.get(); + return tileSource != null ? tileSource.getMinimumZoomLevel() : MINIMUM_ZOOMLEVEL; + } + + @Override + public int getMaximumZoomLevel() { + ITileSource tileSource = mTileSource.get(); + return tileSource != null ? tileSource.getMaximumZoomLevel() : MAXIMUM_ZOOMLEVEL; + } + + @Override + protected void onMediaMounted() { + if (!mSpecificArchivesProvided) { + findArchiveFiles(); + } + } + + @Override + protected void onMediaUnmounted() { + if (!mSpecificArchivesProvided) { + findArchiveFiles(); + } + } + + @Override + public void setTileSource(final ITileSource pTileSource) { + mTileSource.set(pTileSource); + } + + @Override + public void detach() { + while(!mArchiveFiles.isEmpty()) { + mArchiveFiles.remove(0); + } + super.detach(); + } + + // =========================================================== + // Methods + // =========================================================== + + private void findArchiveFiles() { + + mArchiveFiles.clear(); + + if (!getSdCardAvailable()) { + return; + } + + // path should be optionally configurable + final File[] files = OSMDROID_PATH.listFiles(); + if (files != null) { + for (final File file : files) { + final IArchiveFile archiveFile = ArchiveFileFactory.getArchiveFile(file); + if (archiveFile != null) { + mArchiveFiles.add(archiveFile); + } + } + } + } + + private synchronized InputStream getInputStream(final MapTile pTile, + final ITileSource tileSource) { + for (final IArchiveFile archiveFile : mArchiveFiles) { + final InputStream in = archiveFile.getInputStream(tileSource, pTile); + if (in != null) { + if (DEBUGMODE) { + logger.debug("Found tile " + pTile + " in " + archiveFile); + } + return in; + } + } + + return null; + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + protected class TileLoader extends MapTileModuleProviderBase.TileLoader { + + @Override + public Drawable loadTile(final MapTileRequestState pState) { + + ITileSource tileSource = mTileSource.get(); + if (tileSource == null) { + return null; + } + + final MapTile pTile = pState.getMapTile(); + + // if there's no sdcard then don't do anything + if (!getSdCardAvailable()) { + if (DEBUGMODE) { + logger.debug("No sdcard - do nothing for tile: " + pTile); + } + return null; + } + + InputStream inputStream = null; + try { + if (DEBUGMODE) { + logger.debug("Tile doesn't exist: " + pTile); + } + + inputStream = getInputStream(pTile, tileSource); + if (inputStream != null) { + if (DEBUGMODE) { + logger.debug("Use tile from archive: " + pTile); + } + final Drawable drawable = tileSource.getDrawable(inputStream); + return drawable; + } + } catch (final Throwable e) { + logger.error("Error loading tile", e); + } finally { + if (inputStream != null) { + StreamUtils.closeStream(inputStream); + } + } + + return null; + } + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/MapTileFileStorageProviderBase.java b/src/main/java/org/osmdroid/tileprovider/modules/MapTileFileStorageProviderBase.java new file mode 100644 index 000000000..3085daa31 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/MapTileFileStorageProviderBase.java @@ -0,0 +1,86 @@ +package org.osmdroid.tileprovider.modules; + +import org.osmdroid.tileprovider.IRegisterReceiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Environment; + +public abstract class MapTileFileStorageProviderBase extends MapTileModuleProviderBase { + + private static final Logger logger = LoggerFactory.getLogger(MapTileFileStorageProviderBase.class); + + /** whether the sdcard is mounted read/write */ + private boolean mSdCardAvailable = true; + + private final IRegisterReceiver mRegisterReceiver; + private MyBroadcastReceiver mBroadcastReceiver; + + public MapTileFileStorageProviderBase(final IRegisterReceiver pRegisterReceiver, + final int pThreadPoolSize, final int pPendingQueueSize) { + super(pThreadPoolSize, pPendingQueueSize); + + checkSdCard(); + + mRegisterReceiver = pRegisterReceiver; + mBroadcastReceiver = new MyBroadcastReceiver(); + + final IntentFilter mediaFilter = new IntentFilter(); + mediaFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + mediaFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + mediaFilter.addDataScheme("file"); + pRegisterReceiver.registerReceiver(mBroadcastReceiver, mediaFilter); + } + + private void checkSdCard() { + final String state = Environment.getExternalStorageState(); + logger.info("sdcard state: " + state); + mSdCardAvailable = Environment.MEDIA_MOUNTED.equals(state); + } + + protected boolean getSdCardAvailable() { + return mSdCardAvailable; + } + + @Override + public void detach() { + if (mBroadcastReceiver != null) { + mRegisterReceiver.unregisterReceiver(mBroadcastReceiver); + mBroadcastReceiver = null; + } + super.detach(); + } + + protected void onMediaMounted() { + // Do nothing by default. Override to handle. + } + + protected void onMediaUnmounted() { + // Do nothing by default. Override to handle. + } + + /** + * This broadcast receiver will recheck the sd card when the mount/unmount messages happen + * + */ + private class MyBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(final Context aContext, final Intent aIntent) { + + final String action = aIntent.getAction(); + + checkSdCard(); + + if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) { + onMediaMounted(); + } else if (Intent.ACTION_MEDIA_UNMOUNTED.equals(action)) { + onMediaUnmounted(); + } + } + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/MapTileFilesystemProvider.java b/src/main/java/org/osmdroid/tileprovider/modules/MapTileFilesystemProvider.java new file mode 100644 index 000000000..8ec45b2ce --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/MapTileFilesystemProvider.java @@ -0,0 +1,178 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.File; +import java.util.concurrent.atomic.AtomicReference; + +import org.osmdroid.tileprovider.ExpirableBitmapDrawable; +import org.osmdroid.tileprovider.IRegisterReceiver; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.MapTileRequestState; +import org.osmdroid.tileprovider.tilesource.BitmapTileSourceBase.LowMemoryException; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.graphics.drawable.Drawable; + +/** + * Implements a file system cache and provides cached tiles. This functions as a tile provider by + * serving cached tiles for the supplied tile source. + * + * @author Marc Kurtz + * @author Nicolas Gramlich + * + */ +public class MapTileFilesystemProvider extends MapTileFileStorageProviderBase { + + // =========================================================== + // Constants + // =========================================================== + + private static final Logger logger = LoggerFactory.getLogger(MapTileFilesystemProvider.class); + + // =========================================================== + // Fields + // =========================================================== + + private final long mMaximumCachedFileAge; + + private final AtomicReference mTileSource = new AtomicReference(); + + // =========================================================== + // Constructors + // =========================================================== + + public MapTileFilesystemProvider(final IRegisterReceiver pRegisterReceiver) { + this(pRegisterReceiver, TileSourceFactory.DEFAULT_TILE_SOURCE); + } + + public MapTileFilesystemProvider(final IRegisterReceiver pRegisterReceiver, + final ITileSource aTileSource) { + this(pRegisterReceiver, aTileSource, DEFAULT_MAXIMUM_CACHED_FILE_AGE); + } + + public MapTileFilesystemProvider(final IRegisterReceiver pRegisterReceiver, + final ITileSource pTileSource, final long pMaximumCachedFileAge) { + this(pRegisterReceiver, pTileSource, pMaximumCachedFileAge, + NUMBER_OF_TILE_FILESYSTEM_THREADS, + TILE_FILESYSTEM_MAXIMUM_QUEUE_SIZE); + } + + /** + * Provides a file system based cache tile provider. Other providers can register and store data + * in the cache. + * + * @param pRegisterReceiver + */ + public MapTileFilesystemProvider(final IRegisterReceiver pRegisterReceiver, + final ITileSource pTileSource, final long pMaximumCachedFileAge, int pThreadPoolSize, + int pPendingQueueSize) { + super(pRegisterReceiver, pThreadPoolSize, pPendingQueueSize); + setTileSource(pTileSource); + + mMaximumCachedFileAge = pMaximumCachedFileAge; + } + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public boolean getUsesDataConnection() { + return false; + } + + @Override + protected String getName() { + return "File System Cache Provider"; + } + + @Override + protected String getThreadGroupName() { + return "filesystem"; + } + + @Override + protected Runnable getTileLoader() { + return new TileLoader(); + } + + @Override + public int getMinimumZoomLevel() { + ITileSource tileSource = mTileSource.get(); + return tileSource != null ? tileSource.getMinimumZoomLevel() : MINIMUM_ZOOMLEVEL; + } + + @Override + public int getMaximumZoomLevel() { + ITileSource tileSource = mTileSource.get(); + return tileSource != null ? tileSource.getMaximumZoomLevel() : MAXIMUM_ZOOMLEVEL; + } + + @Override + public void setTileSource(final ITileSource pTileSource) { + mTileSource.set(pTileSource); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + protected class TileLoader extends MapTileModuleProviderBase.TileLoader { + + @Override + public Drawable loadTile(final MapTileRequestState pState) throws CantContinueException { + + ITileSource tileSource = mTileSource.get(); + if (tileSource == null) { + return null; + } + + final MapTile tile = pState.getMapTile(); + + // if there's no sdcard then don't do anything + if (!getSdCardAvailable()) { + if (DEBUGMODE) { + logger.debug("No sdcard - do nothing for tile: " + tile); + } + return null; + } + + // Check the tile source to see if its file is available and if so, then render the + // drawable and return the tile + final File file = new File(TILE_PATH_BASE, + tileSource.getTileRelativeFilenameString(tile) + TILE_PATH_EXTENSION); + if (file.exists()) { + + try { + final Drawable drawable = tileSource.getDrawable(file.getPath()); + + // Check to see if file has expired + final long now = System.currentTimeMillis(); + final long lastModified = file.lastModified(); + final boolean fileExpired = lastModified < now - mMaximumCachedFileAge; + + if (fileExpired) { + if (DEBUGMODE) { + logger.debug("Tile expired: " + tile); + } + drawable.setState(new int[] {ExpirableBitmapDrawable.EXPIRED }); + } + + return drawable; + } catch (final LowMemoryException e) { + // low memory so empty the queue + logger.warn("LowMemoryException downloading MapTile: " + tile + " : " + e); + throw new CantContinueException(e); + } + } + + // If we get here then there is no file in the file cache + return null; + } + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/MapTileModuleProviderBase.java b/src/main/java/org/osmdroid/tileprovider/modules/MapTileModuleProviderBase.java new file mode 100644 index 000000000..0f36868b3 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/MapTileModuleProviderBase.java @@ -0,0 +1,325 @@ +package org.osmdroid.tileprovider.modules; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +import org.osmdroid.tileprovider.ExpirableBitmapDrawable; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.MapTileRequestState; +import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.graphics.drawable.Drawable; + +/** + * An abstract base class for modular tile providers + * + * @author Marc Kurtz + * @author Neil Boyd + */ +public abstract class MapTileModuleProviderBase implements OpenStreetMapTileProviderConstants { + + /** + * Gets the human-friendly name assigned to this tile provider. + * + * @return the thread name + */ + protected abstract String getName(); + + /** + * Gets the name assigned to the thread for this provider. + * + * @return the thread name + */ + protected abstract String getThreadGroupName(); + + /** + * It is expected that the implementation will construct an internal member which internally + * implements a {@link TileLoader}. This method is expected to return a that internal member to + * methods of the parent methods. + * + * @return the internal member of this tile provider. + */ + protected abstract Runnable getTileLoader(); + + /** + * Returns true if implementation uses a data connection, false otherwise. This value is used to + * determine if this provider should be skipped if there is no data connection. + * + * @return true if implementation uses a data connection, false otherwise + */ + public abstract boolean getUsesDataConnection(); + + /** + * Gets the minimum zoom level this tile provider can provide + * + * @return the minimum zoom level + */ + public abstract int getMinimumZoomLevel(); + + /** + * Gets the maximum zoom level this tile provider can provide + * + * @return the maximum zoom level + */ + public abstract int getMaximumZoomLevel(); + + /** + * Sets the tile source for this tile provider. + * + * @param tileSource + * the tile source + */ + public abstract void setTileSource(ITileSource tileSource); + + private final ExecutorService mExecutor; + + private static final Logger logger = LoggerFactory.getLogger(MapTileModuleProviderBase.class); + + protected final Object mQueueLockObject = new Object(); + protected final HashMap mWorking; + protected final LinkedHashMap mPending; + + public MapTileModuleProviderBase(int pThreadPoolSize, final int pPendingQueueSize) { + if (pPendingQueueSize < pThreadPoolSize) { + logger.warn("The pending queue size is smaller than the thread pool size. Automatically reducing the thread pool size."); + pThreadPoolSize = pPendingQueueSize; + } + mExecutor = Executors.newFixedThreadPool(pThreadPoolSize, + new ConfigurablePriorityThreadFactory(Thread.NORM_PRIORITY, getThreadGroupName())); + + mWorking = new HashMap(); + mPending = new LinkedHashMap(pPendingQueueSize + 2, 0.1f, + true) { + + private static final long serialVersionUID = 6455337315681858866L; + + @Override + protected boolean removeEldestEntry( + final Map.Entry pEldest) { + if (size() > pPendingQueueSize) { + MapTile result = null; + + // get the oldest tile that isn't in the mWorking queue + Iterator iterator = mPending.keySet().iterator(); + + while (result == null && iterator.hasNext()) { + final MapTile tile = iterator.next(); + if (!mWorking.containsKey(tile)) { + result = tile; + } + } + + if (result != null) { + MapTileRequestState state = mPending.get(result); + removeTileFromQueues(result); + state.getCallback().mapTileRequestFailed(state); + } + } + return false; + } + }; + } + + public void loadMapTileAsync(final MapTileRequestState pState) { + synchronized (mQueueLockObject) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("MapTileModuleProviderBase.loadMaptileAsync() on provider: " + + getName() + " for tile: " + pState.getMapTile()); + if (mPending.containsKey(pState.getMapTile())) + logger.debug("MapTileModuleProviderBase.loadMaptileAsync() tile already exists in request queue for modular provider. Moving to front of queue."); + else + logger.debug("MapTileModuleProviderBase.loadMaptileAsync() adding tile to request queue for modular provider."); + } + + // this will put the tile in the queue, or move it to the front of + // the queue if it's already present + mPending.put(pState.getMapTile(), pState); + } + try { + mExecutor.execute(getTileLoader()); + } catch (final RejectedExecutionException e) { + logger.warn("RejectedExecutionException", e); + } + } + + private void clearQueue() { + synchronized (mQueueLockObject) { + mPending.clear(); + mWorking.clear(); + } + } + + /** + * Detach, we're shutting down - Stops all workers. + */ + public void detach() { + this.clearQueue(); + this.mExecutor.shutdown(); + } + + void removeTileFromQueues(final MapTile mapTile) { + synchronized (mQueueLockObject) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("MapTileModuleProviderBase.removeTileFromQueues() on provider: " + + getName() + " for tile: " + mapTile); + } + mPending.remove(mapTile); + mWorking.remove(mapTile); + } + } + + /** + * Load the requested tile. An abstract internal class whose objects are used by worker threads + * to acquire tiles from servers. It processes tiles from the 'pending' set to the 'working' set + * as they become available. The key unimplemented method is 'loadTile'. + */ + protected abstract class TileLoader implements Runnable { + + /** + * Load the requested tile. + * + * @return the tile if it was loaded successfully, or null if failed to + * load and other tile providers need to be called + * @param pState + * @throws CantContinueException + */ + protected abstract Drawable loadTile(MapTileRequestState pState) + throws CantContinueException; + + protected void onTileLoaderInit() { + // Do nothing by default + } + + protected void onTileLoaderShutdown() { + // Do nothing by default + } + + protected MapTileRequestState nextTile() { + + synchronized (mQueueLockObject) { + MapTile result = null; + + // get the most recently accessed tile + // - the last item in the iterator that's not already being + // processed + Iterator iterator = mPending.keySet().iterator(); + + // TODO this iterates the whole list, make this faster... + while (iterator.hasNext()) { + final MapTile tile = iterator.next(); + if (!mWorking.containsKey(tile)) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("TileLoader.nextTile() on provider: " + getName() + + " found tile in working queue: " + tile); + } + result = tile; + } + } + + if (result != null) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("TileLoader.nextTile() on provider: " + getName() + + " adding tile to working queue: " + result); + } + mWorking.put(result, mPending.get(result)); + } + + return (result != null ? mPending.get(result) : null); + } + } + + /** + * A tile has loaded. + */ + protected void tileLoaded(final MapTileRequestState pState, final Drawable pDrawable) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("TileLoader.tileLoaded() on provider: " + getName() + " with tile: " + + pState.getMapTile()); + } + removeTileFromQueues(pState.getMapTile()); + pState.getCallback().mapTileRequestCompleted(pState, pDrawable); + } + + /** + * A tile has loaded but it's expired. + * Return it and send request to next provider. + */ + protected void tileLoadedExpired(final MapTileRequestState pState, final Drawable pDrawable) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("TileLoader.tileLoadedExpired() on provider: " + getName() + + " with tile: " + pState.getMapTile()); + } + removeTileFromQueues(pState.getMapTile()); + pState.getCallback().mapTileRequestExpiredTile(pState, pDrawable); + } + + protected void tileLoadedFailed(final MapTileRequestState pState) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("TileLoader.tileLoadedFailed() on provider: " + getName() + + " with tile: " + pState.getMapTile()); + } + removeTileFromQueues(pState.getMapTile()); + pState.getCallback().mapTileRequestFailed(pState); + } + + /** + * This is a functor class of type Runnable. The run method is the encapsulated function. + */ + @Override + final public void run() { + + onTileLoaderInit(); + + MapTileRequestState state; + Drawable result = null; + while ((state = nextTile()) != null) { + if (DEBUG_TILE_PROVIDERS) { + logger.debug("TileLoader.run() processing next tile: " + state.getMapTile()); + } + try { + result = null; + result = loadTile(state); + } catch (final CantContinueException e) { + logger.info("Tile loader can't continue: " + state.getMapTile(), e); + clearQueue(); + } catch (final Throwable e) { + logger.error("Error downloading tile: " + state.getMapTile(), e); + } + + if (result == null) { + tileLoadedFailed(state); + } else if (ExpirableBitmapDrawable.isDrawableExpired(result)) { + tileLoadedExpired(state, result); + } else { + tileLoaded(state, result); + } + } + + onTileLoaderShutdown(); + } + } + + /** + * Thrown by a tile provider module in TileLoader.loadTile() to signal that it can no longer + * function properly. This will typically clear the pending queue. + */ + public class CantContinueException extends Exception { + private static final long serialVersionUID = 146526524087765133L; + + public CantContinueException(final String pDetailMessage) { + super(pDetailMessage); + } + + public CantContinueException(final Throwable pThrowable) { + super(pThrowable); + } + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/NetworkAvailabliltyCheck.java b/src/main/java/org/osmdroid/tileprovider/modules/NetworkAvailabliltyCheck.java new file mode 100644 index 000000000..42dd9d467 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/NetworkAvailabliltyCheck.java @@ -0,0 +1,50 @@ +package org.osmdroid.tileprovider.modules; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +/** + * A straightforward network check implementation. NOTE: Requires + * android.permission.ACCESS_NETWORK_STATE and android.permission.ACCESS_WIFI_STATE (?) and + * android.permission.INTERNET (?) + * + * @author Marc Kurtz + * + */ + +public class NetworkAvailabliltyCheck implements INetworkAvailablityCheck { + + private final ConnectivityManager mConnectionManager; + + public NetworkAvailabliltyCheck(final Context aContext) { + mConnectionManager = (ConnectivityManager) aContext + .getSystemService(Context.CONNECTIVITY_SERVICE); + } + + @Override + public boolean getNetworkAvailable() { + final NetworkInfo networkInfo = mConnectionManager.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isAvailable(); + } + + @Override + public boolean getWiFiNetworkAvailable() { + final NetworkInfo wifi = mConnectionManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return wifi != null && wifi.isAvailable(); + } + + @Override + public boolean getCellularDataNetworkAvailable() { + final NetworkInfo mobile = mConnectionManager + .getNetworkInfo(ConnectivityManager.TYPE_MOBILE); + return mobile != null && mobile.isAvailable(); + } + + @Override + public boolean getRouteToPathExists(final int hostAddress) { + return (mConnectionManager.requestRouteToHost(ConnectivityManager.TYPE_WIFI, hostAddress) || mConnectionManager + .requestRouteToHost(ConnectivityManager.TYPE_MOBILE, hostAddress)); + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/TileWriter.java b/src/main/java/org/osmdroid/tileprovider/modules/TileWriter.java new file mode 100644 index 000000000..0d1c60975 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/TileWriter.java @@ -0,0 +1,243 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.tileprovider.util.StreamUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An implementation of {@link IFilesystemCache}. It writes tiles to the file system cache. If the + * cache exceeds 600 Mb then it will be trimmed to 500 Mb. + * + * @author Neil Boyd + * + */ +public class TileWriter implements IFilesystemCache, OpenStreetMapTileProviderConstants { + + // =========================================================== + // Constants + // =========================================================== + + private static final Logger logger = LoggerFactory.getLogger(TileWriter.class); + + // =========================================================== + // Fields + // =========================================================== + + /** amount of disk space used by tile cache **/ + private static long mUsedCacheSpace; + + // =========================================================== + // Constructors + // =========================================================== + + public TileWriter() { + + // do this in the background because it takes a long time + final Thread t = new Thread() { + @Override + public void run() { + mUsedCacheSpace = 0; // because it's static + calculateDirectorySize(TILE_PATH_BASE); + if (mUsedCacheSpace > TILE_MAX_CACHE_SIZE_BYTES) { + cutCurrentCache(); + } + if (DEBUGMODE) { + logger.debug("Finished init thread"); + } + } + }; + t.setPriority(Thread.MIN_PRIORITY); + t.start(); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + /** + * Get the amount of disk space used by the tile cache. This will initially be zero since the + * used space is calculated in the background. + * + * @return size in bytes + */ + public static long getUsedCacheSpace() { + return mUsedCacheSpace; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public boolean saveFile(final ITileSource pTileSource, final MapTile pTile, + final InputStream pStream) { + + final File file = new File(TILE_PATH_BASE, pTileSource.getTileRelativeFilenameString(pTile) + + TILE_PATH_EXTENSION); + + final File parent = file.getParentFile(); + if (!parent.exists() && !createFolderAndCheckIfExists(parent)) { + return false; + } + + BufferedOutputStream outputStream = null; + try { + outputStream = new BufferedOutputStream(new FileOutputStream(file.getPath()), + StreamUtils.IO_BUFFER_SIZE); + final long length = StreamUtils.copy(pStream, outputStream); + + mUsedCacheSpace += length; + if (mUsedCacheSpace > TILE_MAX_CACHE_SIZE_BYTES) { + cutCurrentCache(); // TODO perhaps we should do this in the background + } + } catch (final IOException e) { + return false; + } finally { + if (outputStream != null) { + StreamUtils.closeStream(outputStream); + } + } + return true; + } + + // =========================================================== + // Methods + // =========================================================== + + private boolean createFolderAndCheckIfExists(final File pFile) { + if (pFile.mkdirs()) { + return true; + } + if (DEBUGMODE) { + logger.debug("Failed to create " + pFile + " - wait and check again"); + } + + // if create failed, wait a bit in case another thread created it + try { + Thread.sleep(500); + } catch (final InterruptedException ignore) { + } + // and then check again + if (pFile.exists()) { + if (DEBUGMODE) { + logger.debug("Seems like another thread created " + pFile); + } + return true; + } else { + if (DEBUGMODE) { + logger.debug("File still doesn't exist: " + pFile); + } + return false; + } + } + + private void calculateDirectorySize(final File pDirectory) { + final File[] z = pDirectory.listFiles(); + if (z != null) { + for (final File file : z) { + if (file.isFile()) { + mUsedCacheSpace += file.length(); + } + if (file.isDirectory() && !isSymbolicDirectoryLink(pDirectory, file)) { + calculateDirectorySize(file); // *** recurse *** + } + } + } + } + + /** + * Checks to see if it appears that a directory is a symbolic link. It does this by comparing + * the canonical path of the parent directory and the parent directory of the directory's + * canonical path. If they are equal, then they come from the same true parent. If not, then + * pDirectory is a symbolic link. If we get an exception, we err on the side of caution and + * return "true" expecting the calculateDirectorySize to now skip further processing since + * something went goofy. + */ + private boolean isSymbolicDirectoryLink(final File pParentDirectory, final File pDirectory) { + try { + final String canonicalParentPath1 = pParentDirectory.getCanonicalPath(); + final String canonicalParentPath2 = pDirectory.getCanonicalFile().getParent(); + return !canonicalParentPath1.equals(canonicalParentPath2); + } catch (final IOException e) { + return true; + } catch (final NoSuchElementException e) { + // See: http://code.google.com/p/android/issues/detail?id=4961 + // See: http://code.google.com/p/android/issues/detail?id=5807 + return true; + } + + } + + private List getDirectoryFileList(final File aDirectory) { + final List files = new ArrayList(); + + final File[] z = aDirectory.listFiles(); + if (z != null) { + for (final File file : z) { + if (file.isFile()) { + files.add(file); + } + if (file.isDirectory()) { + files.addAll(getDirectoryFileList(file)); + } + } + } + + return files; + } + + /** + * If the cache size is greater than the max then trim it down to the trim level. This method is + * synchronized so that only one thread can run it at a time. + */ + private void cutCurrentCache() { + + synchronized (TILE_PATH_BASE) { + + if (mUsedCacheSpace > TILE_TRIM_CACHE_SIZE_BYTES) { + + logger.info("Trimming tile cache from " + mUsedCacheSpace + " to " + + TILE_TRIM_CACHE_SIZE_BYTES); + + final List z = getDirectoryFileList(TILE_PATH_BASE); + + // order list by files day created from old to new + final File[] files = z.toArray(new File[0]); + Arrays.sort(files, new Comparator() { + @Override + public int compare(final File f1, final File f2) { + return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()); + } + }); + + for (final File file : files) { + if (mUsedCacheSpace <= TILE_TRIM_CACHE_SIZE_BYTES) { + break; + } + + final long length = file.length(); + if (file.delete()) { + mUsedCacheSpace -= length; + } + } + + logger.info("Finished trimming tile cache"); + } + } + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/modules/ZipFileArchive.java b/src/main/java/org/osmdroid/tileprovider/modules/ZipFileArchive.java new file mode 100644 index 000000000..526ec7378 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/modules/ZipFileArchive.java @@ -0,0 +1,48 @@ +package org.osmdroid.tileprovider.modules; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ZipFileArchive implements IArchiveFile { + + private static final Logger logger = LoggerFactory.getLogger(ZipFileArchive.class); + + private final ZipFile mZipFile; + + private ZipFileArchive(final ZipFile pZipFile) { + mZipFile = pZipFile; + } + + public static ZipFileArchive getZipFileArchive(final File pFile) throws ZipException, IOException { + return new ZipFileArchive(new ZipFile(pFile)); + } + + @Override + public InputStream getInputStream(final ITileSource pTileSource, final MapTile pTile) { + final String path = pTileSource.getTileRelativeFilenameString(pTile); + try { + final ZipEntry entry = mZipFile.getEntry(path); + if (entry != null) { + return mZipFile.getInputStream(entry); + } + } catch (final IOException e) { + logger.warn("Error getting zip stream: " + pTile, e); + } + return null; + } + + @Override + public String toString() { + return "ZipFileArchive [mZipFile=" + mZipFile.getName() + "]"; + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/BitmapTileSourceBase.java b/src/main/java/org/osmdroid/tileprovider/tilesource/BitmapTileSourceBase.java new file mode 100644 index 000000000..0700911b1 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/BitmapTileSourceBase.java @@ -0,0 +1,158 @@ +package org.osmdroid.tileprovider.tilesource; + +import java.io.File; +import java.io.InputStream; +import java.util.Random; + +import org.osmdroid.ResourceProxy; +import org.osmdroid.ResourceProxy.string; +import org.osmdroid.tileprovider.BitmapPool; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.ReusableBitmapDrawable; +import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; + +public abstract class BitmapTileSourceBase implements ITileSource, + OpenStreetMapTileProviderConstants { + + private static final Logger logger = LoggerFactory.getLogger(BitmapTileSourceBase.class); + + private static int globalOrdinal = 0; + + private final int mMinimumZoomLevel; + private final int mMaximumZoomLevel; + + private final int mOrdinal; + protected final String mName; + protected final String mImageFilenameEnding; + protected final Random random = new Random(); + + private final int mTileSizePixels; + + private final string mResourceId; + + public BitmapTileSourceBase(final String aName, final string aResourceId, + final int aZoomMinLevel, final int aZoomMaxLevel, final int aTileSizePixels, + final String aImageFilenameEnding) { + mResourceId = aResourceId; + mOrdinal = globalOrdinal++; + mName = aName; + mMinimumZoomLevel = aZoomMinLevel; + mMaximumZoomLevel = aZoomMaxLevel; + mTileSizePixels = aTileSizePixels; + mImageFilenameEnding = aImageFilenameEnding; + } + + @Override + public int ordinal() { + return mOrdinal; + } + + @Override + public String name() { + return mName; + } + + public String pathBase() { + return mName; + } + + public String imageFilenameEnding() { + return mImageFilenameEnding; + } + + @Override + public int getMinimumZoomLevel() { + return mMinimumZoomLevel; + } + + @Override + public int getMaximumZoomLevel() { + return mMaximumZoomLevel; + } + + @Override + public int getTileSizePixels() { + return mTileSizePixels; + } + + @Override + public String localizedName(final ResourceProxy proxy) { + return proxy.getString(mResourceId); + } + + @Override + public Drawable getDrawable(final String aFilePath) { + try { + // default implementation will load the file as a bitmap and create + // a BitmapDrawable from it + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + BitmapPool.getInstance().applyReusableOptions(bitmapOptions); + final Bitmap bitmap = BitmapFactory.decodeFile(aFilePath, bitmapOptions); + if (bitmap != null) { + return new ReusableBitmapDrawable(bitmap); + } else { + // if we couldn't load it then it's invalid - delete it + try { + new File(aFilePath).delete(); + } catch (final Throwable e) { + logger.error("Error deleting invalid file: " + aFilePath, e); + } + } + } catch (final OutOfMemoryError e) { + logger.error("OutOfMemoryError loading bitmap: " + aFilePath); + System.gc(); + } + return null; + } + + @Override + public String getTileRelativeFilenameString(final MapTile tile) { + final StringBuilder sb = new StringBuilder(); + sb.append(pathBase()); + sb.append('/'); + sb.append(tile.getZoomLevel()); + sb.append('/'); + sb.append(tile.getX()); + sb.append('/'); + sb.append(tile.getY()); + sb.append(imageFilenameEnding()); + return sb.toString(); + } + + @Override + public Drawable getDrawable(final InputStream aFileInputStream) throws LowMemoryException { + try { + // default implementation will load the file as a bitmap and create + // a BitmapDrawable from it + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + BitmapPool.getInstance().applyReusableOptions(bitmapOptions); + final Bitmap bitmap = BitmapFactory.decodeStream(aFileInputStream, null, bitmapOptions); + if (bitmap != null) { + return new ReusableBitmapDrawable(bitmap); + } + } catch (final OutOfMemoryError e) { + logger.error("OutOfMemoryError loading bitmap"); + System.gc(); + throw new LowMemoryException(e); + } + return null; + } + + public final class LowMemoryException extends Exception { + private static final long serialVersionUID = 146526524087765134L; + + public LowMemoryException(final String pDetailMessage) { + super(pDetailMessage); + } + + public LowMemoryException(final Throwable pThrowable) { + super(pThrowable); + } + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/CloudmadeTileSource.java b/src/main/java/org/osmdroid/tileprovider/tilesource/CloudmadeTileSource.java new file mode 100644 index 000000000..f1ec5e4c5 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/CloudmadeTileSource.java @@ -0,0 +1,60 @@ +package org.osmdroid.tileprovider.tilesource; + +import org.osmdroid.ResourceProxy; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.util.CloudmadeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CloudmadeTileSource extends OnlineTileSourceBase implements IStyledTileSource { + + private static final Logger logger = LoggerFactory.getLogger(CloudmadeTileSource.class); + + private Integer mStyle = 1; + + public CloudmadeTileSource(final String pName, final ResourceProxy.string pResourceId, + final int pZoomMinLevel, final int pZoomMaxLevel, final int pTileSizePixels, + final String pImageFilenameEnding, final String... pBaseUrl) { + super(pName, pResourceId, pZoomMinLevel, pZoomMaxLevel, pTileSizePixels, + pImageFilenameEnding, pBaseUrl); + } + + @Override + public String pathBase() { + if (mStyle == null || mStyle <= 1) { + return mName; + } else { + return mName + mStyle; + } + } + + @Override + public String getTileURLString(final MapTile pTile) { + final String key = CloudmadeUtil.getCloudmadeKey(); + if (key.length() == 0) { + logger.error("CloudMade key is not set. You should enter it in the manifest and call CloudmadeUtil.retrieveCloudmadeKey()"); + } + final String token = CloudmadeUtil.getCloudmadeToken(); + return String.format(getBaseUrl(), key, mStyle, getTileSizePixels(), pTile.getZoomLevel(), + pTile.getX(), pTile.getY(), mImageFilenameEnding, token); + } + + @Override + public void setStyle(final Integer pStyle) { + mStyle = pStyle; + } + + @Override + public void setStyle(final String pStyle) { + try { + mStyle = Integer.parseInt(pStyle); + } catch (final NumberFormatException e) { + logger.warn("Error setting integer style: " + pStyle); + } + } + + @Override + public Integer getStyle() { + return mStyle; + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/IStyledTileSource.java b/src/main/java/org/osmdroid/tileprovider/tilesource/IStyledTileSource.java new file mode 100644 index 000000000..1b97787a4 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/IStyledTileSource.java @@ -0,0 +1,15 @@ +package org.osmdroid.tileprovider.tilesource; + +/** + * Tile sources that have a settable "style" attibute can implement this. After setting this on a + * tile provider, you may need to call clearTileCache() or call setTileSource() again on the tile + * provider to clear the current tiles on the screen that are still in the old style. + */ +public interface IStyledTileSource { + + public void setStyle(T style); + + public void setStyle(String style); + + public T getStyle(); +} diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/ITileSource.java b/src/main/java/org/osmdroid/tileprovider/tilesource/ITileSource.java new file mode 100644 index 000000000..ef13d921e --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/ITileSource.java @@ -0,0 +1,85 @@ +package org.osmdroid.tileprovider.tilesource; + +import java.io.InputStream; + +import org.osmdroid.ResourceProxy; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.tilesource.BitmapTileSourceBase.LowMemoryException; + +import android.graphics.drawable.Drawable; + +public interface ITileSource { + + /** + * An ordinal identifier for this tile source + * + * @return the ordinal value + */ + int ordinal(); + + /** + * A human-friendly name for this tile source + * + * @return the tile source name + */ + String name(); + + /** + * A localized human-friendly name for this tile source + * + * @param proxy + * a resource proxy + * @return the localized tile source name + */ + String localizedName(ResourceProxy proxy); + + /** + * Get a unique file path for the tile. This file path may be used to store the tile on a file + * system and performance considerations should be taken into consideration. It can include + * multiple paths. It should not begin with a leading path separator. + * + * @param aTile + * the tile + * @return the unique file path + */ + String getTileRelativeFilenameString(MapTile aTile); + + /** + * Get a rendered Drawable from the specified file path. + * + * @param aFilePath + * a file path + * @return the rendered Drawable + */ + Drawable getDrawable(String aFilePath) throws LowMemoryException; + + /** + * Get a rendered Drawable from the specified InputStream. + * + * @param aTileInputStream + * an InputStream + * @return the rendered Drawable + */ + Drawable getDrawable(InputStream aTileInputStream) throws LowMemoryException; + + /** + * Get the minimum zoom level this tile source can provide. + * + * @return the minimum zoom level + */ + public int getMinimumZoomLevel(); + + /** + * Get the maximum zoom level this tile source can provide. + * + * @return the maximum zoom level + */ + public int getMaximumZoomLevel(); + + /** + * Get the tile size in pixels this tile source provides. + * + * @return the tile size in pixels + */ + public int getTileSizePixels(); +} diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/MapBoxTileSource.java b/src/main/java/org/osmdroid/tileprovider/tilesource/MapBoxTileSource.java new file mode 100644 index 000000000..c3302914f --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/MapBoxTileSource.java @@ -0,0 +1,95 @@ +/** + * @author Brad Leege + * Created on 10/15/13 at 7:57 PM + */ + +package org.osmdroid.tileprovider.tilesource; + +import org.osmdroid.ResourceProxy; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.util.ManifestUtil; + +import android.content.Context; + +public class MapBoxTileSource extends OnlineTileSourceBase +{ + /** the meta data key in the manifest */ + private static final String MAPBOX_MAPID = "MAPBOX_MAPID"; + + private static final String mapBoxBaseUrl = "http://api.tiles.mapbox.com/v3/"; + + private static String mapBoxMapId = ""; + + /** + * TileSource with configuration defaults set. + *
Warning, the static method {@link #retrieveMapBoxMapId(android.content.Context)} should have been invoked once before constructor invocation + */ + public MapBoxTileSource() + { + super("mbtiles", ResourceProxy.string.base, 1, 20, 256, ".png", mapBoxBaseUrl); + } + + /** + * TileSource allowing majority of options (sans url) to be user selected. + *
Warning, the static method {@link #retrieveMapBoxMapId(android.content.Context)} should have been invoked once before constructor invocation + * @param name Name + * @param resourceId Resource Id + * @param zoomMinLevel Minimum Zoom Level + * @param zoomMaxLevel Maximum Zoom Level + * @param tileSizePixels Size of Tile Pixels + * @param imageFilenameEnding Image File Extension + */ + public MapBoxTileSource(String name, ResourceProxy.string resourceId, int zoomMinLevel, int zoomMaxLevel, int tileSizePixels, String imageFilenameEnding) + { + super(name, resourceId, zoomMinLevel, zoomMaxLevel, tileSizePixels, imageFilenameEnding, mapBoxBaseUrl); + } + + /** + * TileSource allowing all options to be user selected. + *
Warning, the static method {@link #retrieveMapBoxMapId(android.content.Context)} should have been invoked once before constructor invocation + * @param name Name + * @param resourceId Resource Id + * @param zoomMinLevel Minimum Zoom Level + * @param zoomMaxLevel Maximum Zoom Level + * @param tileSizePixels Size of Tile Pixels + * @param imageFilenameEnding Image File Extension + * @param mapBoxVersionBaseUrl MapBox Version Base Url @see https://www.mapbox.com/developers/api/#Versions + */ + public MapBoxTileSource(String name, ResourceProxy.string resourceId, int zoomMinLevel, int zoomMaxLevel, int tileSizePixels, String imageFilenameEnding, String mapBoxMapId, String mapBoxVersionBaseUrl) + { + super(name, resourceId, zoomMinLevel, zoomMaxLevel, tileSizePixels, imageFilenameEnding, mapBoxVersionBaseUrl); + } + + /** + * Read the API key from the manifest.
+ * This method should be invoked before class instantiation.
+ */ + public static void retrieveMapBoxMapId(final Context aContext) + { + // Retrieve the MapId from the Manifest + mapBoxMapId = ManifestUtil.retrieveKey(aContext, MAPBOX_MAPID); + } + + public static String getMapBoxMapId() + { + return mapBoxMapId; + } + + @Override + public String getTileURLString(final MapTile aMapTile) + { + StringBuffer url = new StringBuffer(getBaseUrl()); + url.append(getMapBoxMapId()); + url.append("/"); + url.append(aMapTile.getZoomLevel()); + url.append("/"); + url.append(aMapTile.getX()); + url.append("/"); + url.append(aMapTile.getY()); + url.append(".png"); + + String res = url.toString(); + + return res; + } +} \ No newline at end of file diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/OnlineTileSourceBase.java b/src/main/java/org/osmdroid/tileprovider/tilesource/OnlineTileSourceBase.java new file mode 100644 index 000000000..92380162b --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/OnlineTileSourceBase.java @@ -0,0 +1,26 @@ +package org.osmdroid.tileprovider.tilesource; + +import org.osmdroid.ResourceProxy.string; +import org.osmdroid.tileprovider.MapTile; + +public abstract class OnlineTileSourceBase extends BitmapTileSourceBase { + + private final String mBaseUrls[]; + + public OnlineTileSourceBase(final String aName, final string aResourceId, + final int aZoomMinLevel, final int aZoomMaxLevel, final int aTileSizePixels, + final String aImageFilenameEnding, final String... aBaseUrl) { + super(aName, aResourceId, aZoomMinLevel, aZoomMaxLevel, aTileSizePixels, + aImageFilenameEnding); + mBaseUrls = aBaseUrl; + } + + public abstract String getTileURLString(MapTile aTile); + + /** + * Get the base url, which will be a random one if there are more than one. + */ + protected String getBaseUrl() { + return mBaseUrls[random.nextInt(mBaseUrls.length)]; + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/QuadTreeTileSource.java b/src/main/java/org/osmdroid/tileprovider/tilesource/QuadTreeTileSource.java new file mode 100644 index 000000000..48e05d3e8 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/QuadTreeTileSource.java @@ -0,0 +1,42 @@ +package org.osmdroid.tileprovider.tilesource; + +import org.osmdroid.ResourceProxy.string; +import org.osmdroid.tileprovider.MapTile; + +public class QuadTreeTileSource extends OnlineTileSourceBase { + + public QuadTreeTileSource(final String aName, final string aResourceId, + final int aZoomMinLevel, final int aZoomMaxLevel, final int aTileSizePixels, + final String aImageFilenameEnding, final String... aBaseUrl) { + super(aName, aResourceId, aZoomMinLevel, aZoomMaxLevel, aTileSizePixels, + aImageFilenameEnding, aBaseUrl); + } + + @Override + public String getTileURLString(final MapTile aTile) { + return getBaseUrl() + quadTree(aTile) + mImageFilenameEnding; + } + + /** + * Converts TMS tile coordinates to QuadTree + * + * @param aTile + * The tile coordinates to convert + * @return The QuadTree as String. + */ + protected String quadTree(final MapTile aTile) { + final StringBuilder quadKey = new StringBuilder(); + for (int i = aTile.getZoomLevel(); i > 0; i--) { + int digit = 0; + final int mask = 1 << (i - 1); + if ((aTile.getX() & mask) != 0) + digit += 1; + if ((aTile.getY() & mask) != 0) + digit += 2; + quadKey.append("" + digit); + } + + return quadKey.toString(); + } + +} diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/TileSourceFactory.java b/src/main/java/org/osmdroid/tileprovider/tilesource/TileSourceFactory.java new file mode 100644 index 000000000..afcbbc313 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/TileSourceFactory.java @@ -0,0 +1,143 @@ +package org.osmdroid.tileprovider.tilesource; + +import java.util.ArrayList; + +import org.osmdroid.ResourceProxy; + +public class TileSourceFactory { + + // private static final Logger logger = LoggerFactory.getLogger(TileSourceFactory.class); + + /** + * Get the tile source with the specified name. + * + * @param aName + * the tile source name + * @return the tile source + * @throws IllegalArgumentException + * if tile source not found + */ + public static ITileSource getTileSource(final String aName) throws IllegalArgumentException { + for (final ITileSource tileSource : mTileSources) { + if (tileSource.name().equals(aName)) { + return tileSource; + } + } + throw new IllegalArgumentException("No such tile source: " + aName); + } + + public static boolean containsTileSource(final String aName) { + for (final ITileSource tileSource : mTileSources) { + if (tileSource.name().equals(aName)) { + return true; + } + } + return false; + } + + /** + * Get the tile source at the specified position. + * + * @param aOrdinal + * @return the tile source + * @throws IllegalArgumentException + * if tile source not found + */ + public static ITileSource getTileSource(final int aOrdinal) throws IllegalArgumentException { + for (final ITileSource tileSource : mTileSources) { + if (tileSource.ordinal() == aOrdinal) { + return tileSource; + } + } + throw new IllegalArgumentException("No tile source at position: " + aOrdinal); + } + + public static ArrayList getTileSources() { + return mTileSources; + } + + public static void addTileSource(final ITileSource mTileSource) { + mTileSources.add(mTileSource); + } + + public static final OnlineTileSourceBase MAPNIK = new XYTileSource("Mapnik", + ResourceProxy.string.mapnik, 0, 18, 256, ".png", "http://tile.openstreetmap.org/"); + + public static final OnlineTileSourceBase CYCLEMAP = new XYTileSource("CycleMap", + ResourceProxy.string.cyclemap, 0, 17, 256, ".png", + "http://a.tile.opencyclemap.org/cycle/", + "http://b.tile.opencyclemap.org/cycle/", + "http://c.tile.opencyclemap.org/cycle/"); + + public static final OnlineTileSourceBase PUBLIC_TRANSPORT = new XYTileSource( + "OSMPublicTransport", ResourceProxy.string.public_transport, 0, 17, 256, ".png", + "http://openptmap.org/tiles/"); + + public static final OnlineTileSourceBase BASE = new XYTileSource("Base", + ResourceProxy.string.base, 4, 17, 256, ".png", "http://topo.openstreetmap.de/base/"); + + public static final OnlineTileSourceBase TOPO = new XYTileSource("Topo", + ResourceProxy.string.topo, 4, 17, 256, ".png", "http://topo.openstreetmap.de/topo/"); + + public static final OnlineTileSourceBase HILLS = new XYTileSource("Hills", + ResourceProxy.string.hills, 8, 17, 256, ".png", "http://topo.geofabrik.de/hills/"); + + public static final OnlineTileSourceBase CLOUDMADESTANDARDTILES = new CloudmadeTileSource( + "CloudMadeStandardTiles", ResourceProxy.string.cloudmade_standard, 0, 18, 256, ".png", + "http://a.tile.cloudmade.com/%s/%d/%d/%d/%d/%d%s?token=%s", + "http://b.tile.cloudmade.com/%s/%d/%d/%d/%d/%d%s?token=%s", + "http://c.tile.cloudmade.com/%s/%d/%d/%d/%d/%d%s?token=%s"); + + // FYI - This tile source has a tileSize of "6" + public static final OnlineTileSourceBase CLOUDMADESMALLTILES = new CloudmadeTileSource( + "CloudMadeSmallTiles", ResourceProxy.string.cloudmade_small, 0, 21, 64, ".png", + "http://a.tile.cloudmade.com/%s/%d/%d/%d/%d/%d%s?token=%s", + "http://b.tile.cloudmade.com/%s/%d/%d/%d/%d/%d%s?token=%s", + "http://c.tile.cloudmade.com/%s/%d/%d/%d/%d/%d%s?token=%s"); + + public static final OnlineTileSourceBase MAPQUESTOSM = + new XYTileSource("MapquestOSM", ResourceProxy.string.mapquest_osm, 0, 18, 256, ".png", + "http://otile1.mqcdn.com/tiles/1.0.0/map/", + "http://otile2.mqcdn.com/tiles/1.0.0/map/", + "http://otile3.mqcdn.com/tiles/1.0.0/map/", + "http://otile4.mqcdn.com/tiles/1.0.0/map/"); + + public static final OnlineTileSourceBase MAPQUESTAERIAL = + new XYTileSource("MapquestAerial", ResourceProxy.string.mapquest_aerial, 0, 11, 256, ".png", + "http://otile1.mqcdn.com/tiles/1.0.0/sat/", + "http://otile2.mqcdn.com/tiles/1.0.0/sat/", + "http://otile3.mqcdn.com/tiles/1.0.0/sat/", + "http://otile4.mqcdn.com/tiles/1.0.0/sat/"); + + public static final OnlineTileSourceBase DEFAULT_TILE_SOURCE = MAPNIK; + + // The following tile sources are overlays, not standalone map views. + // They are therefore not in mTileSources. + + public static final OnlineTileSourceBase FIETS_OVERLAY_NL = new XYTileSource("Fiets", + ResourceProxy.string.fiets_nl, 3, 18, 256, ".png", + "http://overlay.openstreetmap.nl/openfietskaart-overlay/"); + + public static final OnlineTileSourceBase BASE_OVERLAY_NL = new XYTileSource("BaseNL", + ResourceProxy.string.base_nl, 0, 18, 256, ".png", + "http://overlay.openstreetmap.nl/basemap/"); + + public static final OnlineTileSourceBase ROADS_OVERLAY_NL = new XYTileSource("RoadsNL", + ResourceProxy.string.roads_nl, 0, 18, 256, ".png", + "http://overlay.openstreetmap.nl/roads/"); + + private static ArrayList mTileSources; + static { + mTileSources = new ArrayList(); + mTileSources.add(MAPNIK); + mTileSources.add(CYCLEMAP); + mTileSources.add(PUBLIC_TRANSPORT); + mTileSources.add(BASE); + mTileSources.add(TOPO); + mTileSources.add(HILLS); + mTileSources.add(CLOUDMADESTANDARDTILES); + mTileSources.add(CLOUDMADESMALLTILES); + mTileSources.add(MAPQUESTOSM); + mTileSources.add(MAPQUESTAERIAL); + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/tilesource/XYTileSource.java b/src/main/java/org/osmdroid/tileprovider/tilesource/XYTileSource.java new file mode 100644 index 000000000..23906d6ac --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/tilesource/XYTileSource.java @@ -0,0 +1,20 @@ +package org.osmdroid.tileprovider.tilesource; + +import org.osmdroid.ResourceProxy.string; +import org.osmdroid.tileprovider.MapTile; + +public class XYTileSource extends OnlineTileSourceBase { + + public XYTileSource(final String aName, final string aResourceId, final int aZoomMinLevel, + final int aZoomMaxLevel, final int aTileSizePixels, final String aImageFilenameEnding, + final String... aBaseUrl) { + super(aName, aResourceId, aZoomMinLevel, aZoomMaxLevel, aTileSizePixels, + aImageFilenameEnding, aBaseUrl); + } + + @Override + public String getTileURLString(final MapTile aTile) { + return getBaseUrl() + aTile.getZoomLevel() + "/" + aTile.getX() + "/" + aTile.getY() + + mImageFilenameEnding; + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/util/CloudmadeUtil.java b/src/main/java/org/osmdroid/tileprovider/util/CloudmadeUtil.java new file mode 100644 index 000000000..cc9ee93e1 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/util/CloudmadeUtil.java @@ -0,0 +1,142 @@ +package org.osmdroid.tileprovider.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.osmdroid.http.HttpClientFactory; +import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.preference.PreferenceManager; +import android.provider.Settings; + +/** + * Utility class for implementing Cloudmade authorization. See + * http://developers.cloudmade.com/projects/show/auth + * + * The CloudMade token is persisted because it doesn't change: + * http://support.cloudmade.com/answers/api-keys-and-authentication + * "you will always get the same token for the unique user id" + * + */ +public class CloudmadeUtil implements OpenStreetMapTileProviderConstants { + + private static final Logger logger = LoggerFactory.getLogger(CloudmadeUtil.class); + + /** the meta data key in the manifest */ + private static final String CLOUDMADE_KEY = "CLOUDMADE_KEY"; + + /** the key for the id preference */ + private static final String CLOUDMADE_ID = "CLOUDMADE_ID"; + + /** the key for the token preference */ + private static final String CLOUDMADE_TOKEN = "CLOUDMADE_TOKEN"; + + private static String mAndroidId = Settings.Secure.ANDROID_ID; // will get real id later + + /** the key retrieved from the manifest */ + private static String mKey = ""; + + /** the token */ + private static String mToken = ""; + + private static Editor mPreferenceEditor; + + /** + * Retrieve the key from the manifest and store it for later use. + */ + public static void retrieveCloudmadeKey(final Context aContext) { + + mAndroidId = Settings.Secure.getString(aContext.getContentResolver(), Settings.Secure.ANDROID_ID); + + // get the key from the manifest + mKey = ManifestUtil.retrieveKey(aContext, CLOUDMADE_KEY); + + // if the id hasn't changed then set the token to the previous token + final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(aContext); + mPreferenceEditor = pref.edit(); + final String id = pref.getString(CLOUDMADE_ID, ""); + if (id.equals(mAndroidId)) { + mToken = pref.getString(CLOUDMADE_TOKEN, ""); + // if we've got a token we don't need the editor any more + if (mToken.length() > 0) { + mPreferenceEditor = null; + } + } else { + mPreferenceEditor.putString(CLOUDMADE_ID, mAndroidId); + mPreferenceEditor.commit(); + } + + } + + /** + * Get the key that was previously retrieved from the manifest. + * + * @return the key, or empty string if not found + */ + public static String getCloudmadeKey() { + return mKey; + } + + /** + * Get the token from the Cloudmade server. + * + * @return the token returned from the server, or null if not found + */ + public static String getCloudmadeToken() { + + if (mToken.length() == 0) { + synchronized (mToken) { + // check again because it may have been set while we were blocking + if (mToken.length() == 0) { + final String url = "http://auth.cloudmade.com/token/" + mKey + "?userid=" + mAndroidId; + final HttpClient httpClient = HttpClientFactory.createHttpClient(); + final HttpPost httpPost = new HttpPost(url); + try { + httpPost.setEntity(new StringEntity("", "utf-8")); + final HttpResponse response = httpClient.execute(httpPost); + if (DEBUGMODE) { + logger.debug("Response from Cloudmade auth: " + response.getStatusLine()); + } + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + final BufferedReader br = + new BufferedReader( + new InputStreamReader(response.getEntity().getContent()), + StreamUtils.IO_BUFFER_SIZE); + final String line = br.readLine(); + if (DEBUGMODE) { + logger.debug("First line from Cloudmade auth: " + line); + } + mToken = line.trim(); + if (mToken.length() > 0) { + mPreferenceEditor.putString(CLOUDMADE_TOKEN, mToken); + mPreferenceEditor.commit(); + // we don't need the editor any more + mPreferenceEditor = null; + } else { + logger.error("No authorization token received from Cloudmade"); + } + } + } catch (final IOException e) { + logger.error("No authorization token received from Cloudmade: " + e); + } + } + } + } + + return mToken; + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/util/ManifestUtil.java b/src/main/java/org/osmdroid/tileprovider/util/ManifestUtil.java new file mode 100644 index 000000000..b82ae291d --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/util/ManifestUtil.java @@ -0,0 +1,44 @@ +package org.osmdroid.tileprovider.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +/** + * Utility class for reading the manifest + */ +public class ManifestUtil { + + private static final Logger logger = LoggerFactory.getLogger(ManifestUtil.class); + + /** + * Retrieve a key from the manifest meta data, or empty string if not found. + */ + public static String retrieveKey(final Context aContext, final String aKey) { + + // get the key from the manifest + final PackageManager pm = aContext.getPackageManager(); + try { + final ApplicationInfo info = pm.getApplicationInfo(aContext.getPackageName(), + PackageManager.GET_META_DATA); + if (info.metaData == null) { + logger.info("Key %s not found in manifest", aKey); + } else { + final String value = info.metaData.getString(aKey); + if (value == null) { + logger.info("Key %s not found in manifest", aKey); + } else { + return value.trim(); + } + } + } catch (final PackageManager.NameNotFoundException e) { + logger.info("Key %s not found in manifest", aKey); + } + return ""; + } + + +} diff --git a/src/main/java/org/osmdroid/tileprovider/util/SimpleInvalidationHandler.java b/src/main/java/org/osmdroid/tileprovider/util/SimpleInvalidationHandler.java new file mode 100644 index 000000000..cd7062400 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/util/SimpleInvalidationHandler.java @@ -0,0 +1,25 @@ +package org.osmdroid.tileprovider.util; + +import org.osmdroid.tileprovider.MapTile; + +import android.os.Handler; +import android.os.Message; +import android.view.View; + +public class SimpleInvalidationHandler extends Handler { + private final View mView; + + public SimpleInvalidationHandler(final View pView) { + super(); + mView = pView; + } + + @Override + public void handleMessage(final Message msg) { + switch (msg.what) { + case MapTile.MAPTILE_SUCCESS_ID: + mView.invalidate(); + break; + } + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/util/SimpleRegisterReceiver.java b/src/main/java/org/osmdroid/tileprovider/util/SimpleRegisterReceiver.java new file mode 100644 index 000000000..72d792c17 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/util/SimpleRegisterReceiver.java @@ -0,0 +1,28 @@ +package org.osmdroid.tileprovider.util; + +import org.osmdroid.tileprovider.IRegisterReceiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +public class SimpleRegisterReceiver implements IRegisterReceiver { + + private final Context mContext; + + public SimpleRegisterReceiver(final Context pContext) { + super(); + mContext = pContext; + } + + @Override + public Intent registerReceiver(final BroadcastReceiver aReceiver, final IntentFilter aFilter) { + return mContext.registerReceiver(aReceiver, aFilter); + } + + @Override + public void unregisterReceiver(final BroadcastReceiver aReceiver) { + mContext.unregisterReceiver(aReceiver); + } +} diff --git a/src/main/java/org/osmdroid/tileprovider/util/StreamUtils.java b/src/main/java/org/osmdroid/tileprovider/util/StreamUtils.java new file mode 100644 index 000000000..1fff16031 --- /dev/null +++ b/src/main/java/org/osmdroid/tileprovider/util/StreamUtils.java @@ -0,0 +1,91 @@ +// Created by plusminus on 19:14:08 - 20.10.2008 +package org.osmdroid.tileprovider.util; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StreamUtils { + + // =========================================================== + // Constants + // =========================================================== + + private static final Logger logger = LoggerFactory.getLogger(StreamUtils.class); + + public static final int IO_BUFFER_SIZE = 8 * 1024; + + // =========================================================== + // Fields + // =========================================================== + + // =========================================================== + // Constructors + // =========================================================== + + /** + * This is a utility class with only static members. + */ + private StreamUtils() { + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + /** + * Copy the content of the input stream into the output stream, using a temporary byte array + * buffer whose size is defined by {@link #IO_BUFFER_SIZE}. + * + * @param in + * The input stream to copy from. + * @param out + * The output stream to copy to. + * @return the total length copied + * + * @throws IOException + * If any error occurs during the copy. + */ + public static long copy(final InputStream in, final OutputStream out) throws IOException { + long length = 0; + final byte[] b = new byte[IO_BUFFER_SIZE]; + int read; + while ((read = in.read(b)) != -1) { + out.write(b, 0, read); + length += read; + } + return length; + } + + /** + * Closes the specified stream. + * + * @param stream + * The stream to close. + */ + public static void closeStream(final Closeable stream) { + if (stream != null) { + try { + stream.close(); + } catch (final IOException e) { + logger.error("IO", "Could not close stream", e); + } + } + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/util/BoundingBoxE6.java b/src/main/java/org/osmdroid/util/BoundingBoxE6.java new file mode 100644 index 000000000..1c2918dfe --- /dev/null +++ b/src/main/java/org/osmdroid/util/BoundingBoxE6.java @@ -0,0 +1,270 @@ +// Created by plusminus on 19:06:38 - 25.09.2008 +package org.osmdroid.util; + +import static org.osmdroid.util.MyMath.gudermann; +import static org.osmdroid.util.MyMath.gudermannInverse; + +import java.io.Serializable; +import java.util.ArrayList; + +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.views.util.constants.MapViewConstants; + +import android.graphics.PointF; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * + * @author Nicolas Gramlich + * + */ +public class BoundingBoxE6 implements Parcelable, Serializable, MapViewConstants { + + // =========================================================== + // Constants + // =========================================================== + + static final long serialVersionUID = 2L; + + // =========================================================== + // Fields + // =========================================================== + + protected final int mLatNorthE6; + protected final int mLatSouthE6; + protected final int mLonEastE6; + protected final int mLonWestE6; + + // =========================================================== + // Constructors + // =========================================================== + + public BoundingBoxE6(final int northE6, final int eastE6, final int southE6, final int westE6) { + this.mLatNorthE6 = northE6; + this.mLonEastE6 = eastE6; + this.mLatSouthE6 = southE6; + this.mLonWestE6 = westE6; + } + + public BoundingBoxE6(final double north, final double east, final double south, + final double west) { + this.mLatNorthE6 = (int) (north * 1E6); + this.mLonEastE6 = (int) (east * 1E6); + this.mLatSouthE6 = (int) (south * 1E6); + this.mLonWestE6 = (int) (west * 1E6); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + /** + * @return GeoPoint center of this BoundingBox + */ + public GeoPoint getCenter() { + return new GeoPoint((this.mLatNorthE6 + this.mLatSouthE6) / 2, + (this.mLonEastE6 + this.mLonWestE6) / 2); + } + + public int getDiagonalLengthInMeters() { + return new GeoPoint(this.mLatNorthE6, this.mLonWestE6).distanceTo(new GeoPoint( + this.mLatSouthE6, this.mLonEastE6)); + } + + public int getLatNorthE6() { + return this.mLatNorthE6; + } + + public int getLatSouthE6() { + return this.mLatSouthE6; + } + + public int getLonEastE6() { + return this.mLonEastE6; + } + + public int getLonWestE6() { + return this.mLonWestE6; + } + + public int getLatitudeSpanE6() { + return Math.abs(this.mLatNorthE6 - this.mLatSouthE6); + } + + public int getLongitudeSpanE6() { + return Math.abs(this.mLonEastE6 - this.mLonWestE6); + } + + /** + * + * @param aLatitude + * @param aLongitude + * @param reuse + * @return relative position determined from the upper left corner.
+ * {0,0} would be the upper left corner. {1,1} would be the lower right corner. {1,0} + * would be the lower left corner. {0,1} would be the upper right corner. + */ + public PointF getRelativePositionOfGeoPointInBoundingBoxWithLinearInterpolation( + final int aLatitude, final int aLongitude, final PointF reuse) { + final PointF out = (reuse != null) ? reuse : new PointF(); + final float y = ((float) (this.mLatNorthE6 - aLatitude) / getLatitudeSpanE6()); + final float x = 1 - ((float) (this.mLonEastE6 - aLongitude) / getLongitudeSpanE6()); + out.set(x, y); + return out; + } + + public PointF getRelativePositionOfGeoPointInBoundingBoxWithExactGudermannInterpolation( + final int aLatitudeE6, final int aLongitudeE6, final PointF reuse) { + final PointF out = (reuse != null) ? reuse : new PointF(); + final float y = (float) ((gudermannInverse(this.mLatNorthE6 / 1E6) - gudermannInverse(aLatitudeE6 / 1E6)) / (gudermannInverse(this.mLatNorthE6 / 1E6) - gudermannInverse(this.mLatSouthE6 / 1E6))); + final float x = 1 - ((float) (this.mLonEastE6 - aLongitudeE6) / getLongitudeSpanE6()); + out.set(x, y); + return out; + } + + public GeoPoint getGeoPointOfRelativePositionWithLinearInterpolation(final float relX, + final float relY) { + + int lat = (int) (this.mLatNorthE6 - (this.getLatitudeSpanE6() * relY)); + + int lon = (int) (this.mLonWestE6 + (this.getLongitudeSpanE6() * relX)); + + /* Bring into bounds. */ + while (lat > 90500000) + lat -= 90500000; + while (lat < -90500000) + lat += 90500000; + + /* Bring into bounds. */ + while (lon > 180000000) + lon -= 180000000; + while (lon < -180000000) + lon += 180000000; + + return new GeoPoint(lat, lon); + } + + public GeoPoint getGeoPointOfRelativePositionWithExactGudermannInterpolation(final float relX, + final float relY) { + + final double gudNorth = gudermannInverse(this.mLatNorthE6 / 1E6); + final double gudSouth = gudermannInverse(this.mLatSouthE6 / 1E6); + final double latD = gudermann((gudSouth + (1 - relY) * (gudNorth - gudSouth))); + int lat = (int) (latD * 1E6); + + int lon = (int) ((this.mLonWestE6 + (this.getLongitudeSpanE6() * relX))); + + /* Bring into bounds. */ + while (lat > 90500000) + lat -= 90500000; + while (lat < -90500000) + lat += 90500000; + + /* Bring into bounds. */ + while (lon > 180000000) + lon -= 180000000; + while (lon < -180000000) + lon += 180000000; + + return new GeoPoint(lat, lon); + } + + public BoundingBoxE6 increaseByScale(final float pBoundingboxPaddingRelativeScale) { + final GeoPoint pCenter = this.getCenter(); + final int mLatSpanE6Padded_2 = (int) ((this.getLatitudeSpanE6() * pBoundingboxPaddingRelativeScale) / 2); + final int mLonSpanE6Padded_2 = (int) ((this.getLongitudeSpanE6() * pBoundingboxPaddingRelativeScale) / 2); + + return new BoundingBoxE6(pCenter.getLatitudeE6() + mLatSpanE6Padded_2, + pCenter.getLongitudeE6() + mLonSpanE6Padded_2, pCenter.getLatitudeE6() + - mLatSpanE6Padded_2, pCenter.getLongitudeE6() - mLonSpanE6Padded_2); + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public String toString() { + return new StringBuffer().append("N:").append(this.mLatNorthE6).append("; E:") + .append(this.mLonEastE6).append("; S:").append(this.mLatSouthE6).append("; W:") + .append(this.mLonWestE6).toString(); + } + + // =========================================================== + // Methods + // =========================================================== + + public GeoPoint bringToBoundingBox(final int aLatitudeE6, final int aLongitudeE6) { + return new GeoPoint(Math.max(this.mLatSouthE6, Math.min(this.mLatNorthE6, aLatitudeE6)), + Math.max(this.mLonWestE6, Math.min(this.mLonEastE6, aLongitudeE6))); + } + + public static BoundingBoxE6 fromGeoPoints(final ArrayList partialPolyLine) { + int minLat = Integer.MAX_VALUE; + int minLon = Integer.MAX_VALUE; + int maxLat = Integer.MIN_VALUE; + int maxLon = Integer.MIN_VALUE; + for (final GeoPoint gp : partialPolyLine) { + final int latitudeE6 = gp.getLatitudeE6(); + final int longitudeE6 = gp.getLongitudeE6(); + + minLat = Math.min(minLat, latitudeE6); + minLon = Math.min(minLon, longitudeE6); + maxLat = Math.max(maxLat, latitudeE6); + maxLon = Math.max(maxLon, longitudeE6); + } + + return new BoundingBoxE6(maxLat, maxLon, minLat, minLon); + } + + public boolean contains(final IGeoPoint pGeoPoint) { + return contains(pGeoPoint.getLatitudeE6(), pGeoPoint.getLongitudeE6()); + } + + public boolean contains(final int aLatitudeE6, final int aLongitudeE6) { + return ((aLatitudeE6 < this.mLatNorthE6) && (aLatitudeE6 > this.mLatSouthE6)) + && ((aLongitudeE6 < this.mLonEastE6) && (aLongitudeE6 > this.mLonWestE6)); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + // =========================================================== + // Parcelable + // =========================================================== + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public BoundingBoxE6 createFromParcel(final Parcel in) { + return readFromParcel(in); + } + + @Override + public BoundingBoxE6[] newArray(final int size) { + return new BoundingBoxE6[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel out, final int arg1) { + out.writeInt(this.mLatNorthE6); + out.writeInt(this.mLonEastE6); + out.writeInt(this.mLatSouthE6); + out.writeInt(this.mLonWestE6); + } + + private static BoundingBoxE6 readFromParcel(final Parcel in) { + final int latNorthE6 = in.readInt(); + final int lonEastE6 = in.readInt(); + final int latSouthE6 = in.readInt(); + final int lonWestE6 = in.readInt(); + return new BoundingBoxE6(latNorthE6, lonEastE6, latSouthE6, lonWestE6); + } +} diff --git a/src/main/java/org/osmdroid/util/GEMFFile.java b/src/main/java/org/osmdroid/util/GEMFFile.java new file mode 100644 index 000000000..e88f593ca --- /dev/null +++ b/src/main/java/org/osmdroid/util/GEMFFile.java @@ -0,0 +1,696 @@ +package org.osmdroid.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * GEMF File handler class. + * + * Reference: https://sites.google.com/site/abudden/android-map-store + * + * @author A. S. Budden + * @author Erik Burrows + * + */ +public class GEMFFile { + + // =========================================================== + // Constants + // =========================================================== + + private static final long FILE_SIZE_LIMIT = 1 * 1024 * 1024 * 1024; // 1GB + private static final int FILE_COPY_BUFFER_SIZE = 1024; + + private static final int VERSION = 4; + private static final int TILE_SIZE = 256; + + private static final int U32_SIZE = 4; + private static final int U64_SIZE = 8; + + + // =========================================================== + // Fields + // =========================================================== + + // Path to first GEMF file (additional files as -1, -2, ... + private final String mLocation; + + // All GEMF file parts for this archive + private final List mFiles = new ArrayList(); + private final List mFileNames = new ArrayList(); + + // Tile ranges represented within this archive + private final List mRangeData = new ArrayList(); + + // File sizes for offset calculation + private final List mFileSizes = new ArrayList(); + + // List of tile sources within this archive + private final LinkedHashMap mSources = new LinkedHashMap(); + + // Fields to restrict to a single source for reading + private boolean mSourceLimited = false; + private int mCurrentSource = 0; + + + // =========================================================== + // Constructors + // =========================================================== + + + /* + * Constructor to read existing GEMF archive + * + * @param pLocation + * File object representing first GEMF archive file + */ + public GEMFFile (final File pLocation) throws FileNotFoundException, IOException { + this(pLocation.getAbsolutePath()); + } + + + /* + * Constructor to read existing GEMF archive + * + * @param pLocation + * String object representing path to first GEMF archive file + */ + public GEMFFile (final String pLocation) throws FileNotFoundException, IOException { + mLocation = pLocation; + openFiles(); + readHeader(); + } + + + /* + * Constructor to create new GEMF file from directory of sources/tiles. + * + * @param pLocation + * String object representing path to first GEMF archive file. + * Additional files (if archive size exceeds FILE_SIZE_LIMIT + * will be created with numerical suffixes, eg: test.gemf-1, test.gemf-2. + * @param pSourceFolders + * Each specified folder will be imported into the GEMF archive as a seperate + * source. The name of the folder will be the name of the source in the archive. + */ + public GEMFFile (final String pLocation, final List pSourceFolders) + throws FileNotFoundException, IOException { + /* + * 1. For each source folder + * 1. Create array of zoom levels, X rows, Y rows + * 2. Build index data structure index[source][zoom][range] + * 1. For each S-Z-X find list of Ys values + * 2. For each S-Z-X-Ys set, find complete X ranges + * 3. For each S-Z-Xr-Ys set, find complete Y ranges, create Range record + * 3. Write out index + * 1. Header + * 2. Sources + * 3. For each Range + * 1. Write Range record + * 4. For each Range record + * 1. For each Range entry + * 1. If over file size limit, start new data file + * 2. Write tile data + */ + + this.mLocation = pLocation; + + // Create in-memory array of sources, X and Y values. + final LinkedHashMap>>> dirIndex = + new LinkedHashMap>>>(); + + for (final File sourceDir: pSourceFolders) { + + final LinkedHashMap>> zList = + new LinkedHashMap>>(); + + for (final File zDir: sourceDir.listFiles()) { + // Make sure the directory name is just a number + try { + Integer.parseInt(zDir.getName()); + } catch (final NumberFormatException e) { + continue; + } + + final LinkedHashMap> xList = + new LinkedHashMap>(); + + for (final File xDir: zDir.listFiles()) { + + // Make sure the directory name is just a number + try { + Integer.parseInt(xDir.getName()); + } catch (final NumberFormatException e) { + continue; + } + + final LinkedHashMap yList = new LinkedHashMap(); + for (final File yFile: xDir.listFiles()) { + + try { + Integer.parseInt(yFile.getName().substring( + 0, yFile.getName().indexOf('.'))); + } catch (final NumberFormatException e) { + continue; + } + + yList.put(Integer.parseInt(yFile.getName().substring( + 0, yFile.getName().indexOf('.'))), yFile); + } + + xList.put(new Integer(xDir.getName()), yList); + } + + zList.put(Integer.parseInt(zDir.getName()), xList); + } + + dirIndex.put(sourceDir.getName(), zList); + } + + // Create a source index list + final LinkedHashMap sourceIndex = new LinkedHashMap(); + final LinkedHashMap indexSource = new LinkedHashMap(); + int si = 0; + for (final String source: dirIndex.keySet()) { + sourceIndex.put(source, new Integer(si)); + indexSource.put(new Integer(si), source); + ++si; + } + + // Create the range objects + final List ranges = new ArrayList(); + + for (final String source: dirIndex.keySet()) { + for (final Integer zoom: dirIndex.get(source).keySet()) { + + // Get non-contiguous Y sets for each Z/X + final LinkedHashMap, List> ySets = + new LinkedHashMap, List>(); + + for (final Integer x: new TreeSet(dirIndex.get(source).get(zoom).keySet())) { + + final List ySet = new ArrayList(); + for (final Integer y: dirIndex.get(source).get(zoom).get(x).keySet()) { + ySet.add(y); + } + + if (ySet.size() == 0) { + continue; + } + + Collections.sort(ySet); + + if (! ySets.containsKey(ySet)) { + ySets.put(ySet, new ArrayList()); + } + + ySets.get(ySet).add(x); + } + + // For each Y set find contiguous X sets + final LinkedHashMap, List> xSets = + new LinkedHashMap, List>(); + + for (final List ySet: ySets.keySet()) { + + final TreeSet xList = new TreeSet(ySets.get(ySet)); + + List xSet = new ArrayList(); + for(int i = xList.first(); i < xList.last() + 1; ++i) { + if (xList.contains(new Integer(i))) { + xSet.add(new Integer(i)); + } else { + if (xSet.size() > 0) { + xSets.put(ySet, xSet); + xSet = new ArrayList(); + } + } + } + + if (xSet.size() > 0) { + xSets.put(ySet, xSet); + } + } + + // For each contiguous X set, find contiguous Y sets and create GEMFRange object + for (final List xSet: xSets.keySet()) { + + final TreeSet yList = new TreeSet(xSet); + final TreeSet xList = new TreeSet(ySets.get(xSet)); + + GEMFRange range = new GEMFFile.GEMFRange(); + range.zoom = zoom; + range.sourceIndex = sourceIndex.get(source); + range.xMin = xList.first(); + range.xMax = xList.last(); + + for(int i = yList.first(); i < yList.last() + 1; ++i) { + if (yList.contains(new Integer(i))) { + if (range.yMin == null) { + range.yMin = i; + } + range.yMax = i; + } else { + + if (range.yMin != null) { + ranges.add(range); + + range = new GEMFFile.GEMFRange(); + range.zoom = zoom; + range.sourceIndex = sourceIndex.get(source); + range.xMin = xList.first(); + range.xMax = xList.last(); + } + } + } + + if (range.yMin != null) { + ranges.add(range); + } + } + } + } + + + // Calculate size of header for computation of data offsets + int source_list_size = 0; + for (final String source: sourceIndex.keySet()) { + source_list_size += (U32_SIZE + U32_SIZE + source.length()); + } + + long offset = + U32_SIZE + // GEMF Version + U32_SIZE + // Tile size + U32_SIZE + // Number of sources + source_list_size + + ranges.size() * ((U32_SIZE * 6) + U64_SIZE) + + U32_SIZE; // Number of ranges + + // Calculate offset for each range in the data set + for (final GEMFRange range: ranges) { + range.offset = offset; + + for (int x = range.xMin; x < range.xMax + 1; ++x) { + for (int y = range.yMin; y < range.yMax + 1; ++y) { + offset += (U32_SIZE + U64_SIZE); + } + } + } + + final long headerSize = offset; + + RandomAccessFile gemfFile = new RandomAccessFile(pLocation, "rw"); + + // Write version header + gemfFile.writeInt(VERSION); + + // Write file size header + gemfFile.writeInt(TILE_SIZE); + + // Write number of sources + gemfFile.writeInt(sourceIndex.size()); + + // Write source list + for (final String source: sourceIndex.keySet()) { + gemfFile.writeInt(sourceIndex.get(source)); + gemfFile.writeInt(source.length()); + gemfFile.write(source.getBytes()); + } + + // Write number of ranges + gemfFile.writeInt(ranges.size()); + + // Write range objects + for (final GEMFRange range: ranges) { + gemfFile.writeInt(range.zoom); + gemfFile.writeInt(range.xMin); + gemfFile.writeInt(range.xMax); + gemfFile.writeInt(range.yMin); + gemfFile.writeInt(range.yMax); + gemfFile.writeInt(range.sourceIndex); + gemfFile.writeLong(range.offset); + } + + // Write file offset list + for (final GEMFRange range: ranges) { + for (int x = range.xMin; x < range.xMax + 1; ++x) { + for (int y = range.yMin; y < range.yMax + 1; ++y) { + gemfFile.writeLong(offset); + final long fileSize = dirIndex.get( + indexSource.get( + range.sourceIndex)).get(range.zoom).get(x).get(y).length(); + gemfFile.writeInt((int)fileSize); + offset += fileSize; + } + } + } + + // + // Write tiles + // + + final byte[] buf = new byte[FILE_COPY_BUFFER_SIZE]; + + long currentOffset = headerSize; + int fileIndex = 0; + + for (final GEMFRange range: ranges) { + for (int x = range.xMin; x < range.xMax + 1; ++x) { + for (int y = range.yMin; y < range.yMax + 1; ++y) { + + final long fileSize = dirIndex.get( + indexSource.get(range.sourceIndex)).get(range.zoom).get(x).get(y).length(); + + if (currentOffset + fileSize > FILE_SIZE_LIMIT) { + gemfFile.close(); + ++fileIndex; + gemfFile = new RandomAccessFile(pLocation + "-" + fileIndex, "rw"); + currentOffset = 0; + } else { + currentOffset += fileSize; + } + + final FileInputStream tile = new FileInputStream( + dirIndex.get( + indexSource.get( + range.sourceIndex)).get(range.zoom).get(x).get(y)); + + int read = tile.read(buf, 0, FILE_COPY_BUFFER_SIZE); + while (read != -1) { + gemfFile.write(buf, 0, read); + read = tile.read(buf, 0, FILE_COPY_BUFFER_SIZE); + } + + tile.close(); + } + } + } + + gemfFile.close(); + + // Complete construction of GEMFFile object + openFiles(); + readHeader(); + } + + + // =========================================================== + // Private Methods + // =========================================================== + + + /* + * Close open GEMF file handles. + */ + public void close() throws IOException { + for (final RandomAccessFile file: mFiles) { + file.close(); + } + } + + + /* + * Find all files composing this GEMF archive, open them as RandomAccessFile + * and add to the mFiles list. + */ + private void openFiles() throws FileNotFoundException { + // Populate the mFiles array + + final File base = new File(mLocation); + mFiles.add(new RandomAccessFile(base, "r")); + mFileNames.add(base.getPath()); + + int i = 0; + for(;;) { + i = i + 1; + final File nextFile = new File(mLocation + "-" + i); + if (nextFile.exists()) { + mFiles.add(new RandomAccessFile(nextFile, "r")); + mFileNames.add(nextFile.getPath()); + } else { + break; + } + } + } + + + /* + * Read header of archive, cache Ranges. + */ + private void readHeader() throws IOException { + final RandomAccessFile baseFile = mFiles.get(0); + + // Get file sizes + for (final RandomAccessFile file : mFiles) { + mFileSizes.add(file.length()); + } + + // Version + final int version = baseFile.readInt(); + if (version != VERSION) { + throw new IOException("Bad file version: " + version); + } + + // Tile Size + final int tile_size = baseFile.readInt(); + if (tile_size != TILE_SIZE) { + throw new IOException("Bad tile size: " + tile_size); + } + + // Read Source List + final int sourceCount = baseFile.readInt(); + + for (int i=0;i getSources() { + return mSources; + } + + /* + * Set single source for getInputStream() to use. Otherwise, first tile found + * with specified Z/X/Y coordinates will be returned. + */ + public void selectSource(final int pSource) { + if (mSources.containsKey(new Integer(pSource))) { + mSourceLimited = true; + mCurrentSource = pSource; + } + } + + /* + * Allow getInputStream() to use any source in the archive. + */ + public void acceptAnySource() { + mSourceLimited = false; + } + + /* + * Return list of zoom levels contained within this archive. + */ + public Set getZoomLevels() { + final Set zoomLevels = new TreeSet(); + + for (final GEMFRange rs: mRangeData) { + zoomLevels.add(rs.zoom); + } + + return zoomLevels; + } + + /* + * Get an InputStream for the tile data specified by the Z/X/Y coordinates. + * + * @return InputStream of tile data, or null if not found. + */ + public InputStream getInputStream(final int pX, final int pY, final int pZ) { + GEMFRange range = null; + + for (final GEMFRange rs: mRangeData) + { + if ((pZ == rs.zoom) + && (pX >= rs.xMin) + && (pX <= rs.xMax) + && (pY >= rs.yMin) + && (pY <= rs.yMax) + && (( ! mSourceLimited) || (rs.sourceIndex == mCurrentSource))) { + range = rs; + break; + } + } + + if (range == null) { + return null; + } + + long dataOffset; + int dataLength; + + try { + + // Determine offset to requested tile record in the header + final int numY = range.yMax + 1 - range.yMin; + final int xIndex = pX - range.xMin; + final int yIndex = pY - range.yMin; + long offset = (xIndex * numY) + yIndex; + offset *= (U32_SIZE + U64_SIZE); + offset += range.offset; + + + // Read tile record from header, get offset and size of data record + final RandomAccessFile baseFile = mFiles.get(0); + baseFile.seek(offset); + dataOffset = baseFile.readLong(); + dataLength = baseFile.readInt(); + + // Seek to correct data file and offset. + RandomAccessFile pDataFile = mFiles.get(0); + int index = 0; + if (dataOffset > mFileSizes.get(0)) { + final int fileListCount = mFileSizes.size(); + + while ((index < (fileListCount - 1)) && + (dataOffset > mFileSizes.get(index))) { + + dataOffset -= mFileSizes.get(index); + index += 1; + } + + pDataFile = mFiles.get(index); + } + + // Read data block into a byte array + pDataFile.seek(dataOffset); + + return new GEMFInputStream(mFileNames.get(index), dataOffset, dataLength); + + } catch (final java.io.IOException e) { + return null; + } + } + + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + // Class to represent a range of stored tiles within the archive. + private class GEMFRange { + Integer zoom; + Integer xMin; + Integer xMax; + Integer yMin; + Integer yMax; + Integer sourceIndex; + Long offset; + + @Override + public String toString() { + return String.format( + "GEMF Range: source=%d, zoom=%d, x=%d-%d, y=%d-%d, offset=0x%08X", + sourceIndex, zoom, xMin, xMax, yMin, yMax, offset); + } + } + + // InputStream class to hand to the tile loader system. It wants an InputStream, and it is more + // efficient to create a new open file handle pointed to the right place, than to buffer the file + // in memory. + class GEMFInputStream extends InputStream { + + RandomAccessFile raf; + int remainingBytes; + + GEMFInputStream(final String filePath, final long offset, final int length) throws IOException { + this.raf = new RandomAccessFile(filePath, "r"); + raf.seek(offset); + + this.remainingBytes = length; + } + + @Override + public int available() { + return remainingBytes; + } + + @Override + public void close() throws IOException { + raf.close(); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int read(final byte[] buffer, final int offset, final int length) throws IOException { + final int read = raf.read(buffer, offset, length > remainingBytes ? remainingBytes : length); + + remainingBytes -= read; + return read; + } + + @Override + public int read() throws IOException { + if (remainingBytes > 0) { + remainingBytes--; + return raf.read(); + } else { + throw new IOException("End of stream"); + } + } + + @Override + public long skip(final long byteCount) { + return 0; + } + } +} diff --git a/src/main/java/org/osmdroid/util/GeoPoint.java b/src/main/java/org/osmdroid/util/GeoPoint.java new file mode 100644 index 000000000..f70541519 --- /dev/null +++ b/src/main/java/org/osmdroid/util/GeoPoint.java @@ -0,0 +1,330 @@ +// Created by plusminus on 21:28:12 - 25.09.2008 +package org.osmdroid.util; + +import java.io.Serializable; + +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.util.constants.GeoConstants; +import org.osmdroid.views.util.constants.MathConstants; + +import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * + * @author Nicolas Gramlich + * @author Theodore Hong + * + */ +public class GeoPoint implements IGeoPoint, MathConstants, GeoConstants, Parcelable, Serializable, Cloneable { + + // =========================================================== + // Constants + // =========================================================== + + static final long serialVersionUID = 1L; + + // =========================================================== + // Fields + // =========================================================== + + private int mLongitudeE6; + private int mLatitudeE6; + private int mAltitude; + + // =========================================================== + // Constructors + // =========================================================== + + public GeoPoint(final int aLatitudeE6, final int aLongitudeE6) { + this.mLatitudeE6 = aLatitudeE6; + this.mLongitudeE6 = aLongitudeE6; + } + + public GeoPoint(final int aLatitudeE6, final int aLongitudeE6, final int aAltitude) { + this.mLatitudeE6 = aLatitudeE6; + this.mLongitudeE6 = aLongitudeE6; + this.mAltitude = aAltitude; + } + + public GeoPoint(final double aLatitude, final double aLongitude) { + this.mLatitudeE6 = (int) (aLatitude * 1E6); + this.mLongitudeE6 = (int) (aLongitude * 1E6); + } + + public GeoPoint(final double aLatitude, final double aLongitude, final double aAltitude) { + this.mLatitudeE6 = (int) (aLatitude * 1E6); + this.mLongitudeE6 = (int) (aLongitude * 1E6); + this.mAltitude = (int) aAltitude; + } + + public GeoPoint(final Location aLocation) { + this(aLocation.getLatitude(), aLocation.getLongitude(), aLocation.getAltitude()); + } + + public GeoPoint(final GeoPoint aGeopoint) { + this.mLatitudeE6 = aGeopoint.mLatitudeE6; + this.mLongitudeE6 = aGeopoint.mLongitudeE6; + this.mAltitude = aGeopoint.mAltitude; + } + + public static GeoPoint fromDoubleString(final String s, final char spacer) { + final int spacerPos1 = s.indexOf(spacer); + final int spacerPos2 = s.indexOf(spacer, spacerPos1 + 1); + + if (spacerPos2 == -1) { + return new GeoPoint( + (int) (Double.parseDouble(s.substring(0, spacerPos1)) * 1E6), + (int) (Double.parseDouble(s.substring(spacerPos1 + 1, s.length())) * 1E6)); + } else { + return new GeoPoint( + (int) (Double.parseDouble(s.substring(0, spacerPos1)) * 1E6), + (int) (Double.parseDouble(s.substring(spacerPos1 + 1, spacerPos2)) * 1E6), + (int) Double.parseDouble(s.substring(spacerPos2 + 1, s.length()))); + } + } + + public static GeoPoint fromInvertedDoubleString(final String s, final char spacer) { + final int spacerPos1 = s.indexOf(spacer); + final int spacerPos2 = s.indexOf(spacer, spacerPos1 + 1); + + if (spacerPos2 == -1) { + return new GeoPoint( + (int) (Double.parseDouble(s.substring(spacerPos1 + 1, s.length())) * 1E6), + (int) (Double.parseDouble(s.substring(0, spacerPos1)) * 1E6)); + } else { + return new GeoPoint( + (int) (Double.parseDouble(s.substring(spacerPos1 + 1, spacerPos2)) * 1E6), + (int) (Double.parseDouble(s.substring(0, spacerPos1)) * 1E6), + (int) Double.parseDouble(s.substring(spacerPos2 + 1, s.length()))); + + } + } + + public static GeoPoint fromIntString(final String s) { + final int commaPos1 = s.indexOf(','); + final int commaPos2 = s.indexOf(',', commaPos1 + 1); + + if (commaPos2 == -1) { + return new GeoPoint( + Integer.parseInt(s.substring(0, commaPos1)), + Integer.parseInt(s.substring(commaPos1 + 1, s.length()))); + } else { + return new GeoPoint( + Integer.parseInt(s.substring(0, commaPos1)), + Integer.parseInt(s.substring(commaPos1 + 1, commaPos2)), + Integer.parseInt(s.substring(commaPos2 + 1, s.length())) + ); + } + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + @Override + public int getLongitudeE6() { + return this.mLongitudeE6; + } + + @Override + public int getLatitudeE6() { + return this.mLatitudeE6; + } + + @Override + public double getLongitude() { + return this.mLongitudeE6 / 1E6; + } + + @Override + public double getLatitude() { + return this.mLatitudeE6 / 1E6; + } + + public int getAltitude() { + return this.mAltitude; + } + + public void setLongitudeE6(final int aLongitudeE6) { + this.mLongitudeE6 = aLongitudeE6; + } + + public void setLatitudeE6(final int aLatitudeE6) { + this.mLatitudeE6 = aLatitudeE6; + } + + public void setAltitude(final int aAltitude) { + this.mAltitude = aAltitude; + } + + public void setCoordsE6(final int aLatitudeE6, final int aLongitudeE6) { + this.mLatitudeE6 = aLatitudeE6; + this.mLongitudeE6 = aLongitudeE6; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public Object clone() { + return new GeoPoint(this.mLatitudeE6, this.mLongitudeE6); + } + + @Override + public String toString() { + return new StringBuilder().append(this.mLatitudeE6).append(",").append(this.mLongitudeE6).append(",").append(this.mAltitude) + .toString(); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (obj.getClass() != getClass()) { + return false; + } + final GeoPoint rhs = (GeoPoint) obj; + return rhs.mLatitudeE6 == this.mLatitudeE6 && rhs.mLongitudeE6 == this.mLongitudeE6 && rhs.mAltitude == this.mAltitude; + } + + @Override + public int hashCode() { + return 37 * (17 * mLatitudeE6 + mLongitudeE6) + mAltitude; + } + + // =========================================================== + // Parcelable + // =========================================================== + private GeoPoint(final Parcel in) { + this.mLatitudeE6 = in.readInt(); + this.mLongitudeE6 = in.readInt(); + this.mAltitude = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel out, final int flags) { + out.writeInt(mLatitudeE6); + out.writeInt(mLongitudeE6); + out.writeInt(mAltitude); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public GeoPoint createFromParcel(final Parcel in) { + return new GeoPoint(in); + } + + @Override + public GeoPoint[] newArray(final int size) { + return new GeoPoint[size]; + } + }; + + // =========================================================== + // Methods + // =========================================================== + + /** + * @see GPSDistance.html + * @return distance in meters + */ + public int distanceTo(final IGeoPoint other) { + + final double a1 = DEG2RAD * this.mLatitudeE6 / 1E6; + final double a2 = DEG2RAD * this.mLongitudeE6 / 1E6; + final double b1 = DEG2RAD * other.getLatitudeE6() / 1E6; + final double b2 = DEG2RAD * other.getLongitudeE6() / 1E6; + + final double cosa1 = Math.cos(a1); + final double cosb1 = Math.cos(b1); + + final double t1 = cosa1 * Math.cos(a2) * cosb1 * Math.cos(b2); + + final double t2 = cosa1 * Math.sin(a2) * cosb1 * Math.sin(b2); + + final double t3 = Math.sin(a1) * Math.sin(b1); + + final double tt = Math.acos(t1 + t2 + t3); + + return (int) (RADIUS_EARTH_METERS * tt); + } + + /** + * @see discussion + * @return bearing in degrees + */ + public double bearingTo(final IGeoPoint other) { + final double lat1 = Math.toRadians(this.mLatitudeE6 / 1E6); + final double long1 = Math.toRadians(this.mLongitudeE6 / 1E6); + final double lat2 = Math.toRadians(other.getLatitudeE6() / 1E6); + final double long2 = Math.toRadians(other.getLongitudeE6() / 1E6); + final double delta_long = long2 - long1; + final double a = Math.sin(delta_long) * Math.cos(lat2); + final double b = Math.cos(lat1) * Math.sin(lat2) - + Math.sin(lat1) * Math.cos(lat2) * Math.cos(delta_long); + final double bearing = Math.toDegrees(Math.atan2(a, b)); + final double bearing_normalized = (bearing + 360) % 360; + return bearing_normalized; + } + + /** + * Calculate a point that is the specified distance and bearing away from this point. + * + * @see latlong.html + * @see latlon.js + */ + public GeoPoint destinationPoint(final double aDistanceInMeters, final float aBearingInDegrees) { + + // convert distance to angular distance + final double dist = aDistanceInMeters / RADIUS_EARTH_METERS; + + // convert bearing to radians + final float brng = DEG2RAD * aBearingInDegrees; + + // get current location in radians + final double lat1 = DEG2RAD * getLatitudeE6() / 1E6; + final double lon1 = DEG2RAD * getLongitudeE6() / 1E6; + + final double lat2 = Math.asin(Math.sin(lat1) * Math.cos(dist) + Math.cos(lat1) + * Math.sin(dist) * Math.cos(brng)); + final double lon2 = lon1 + + Math.atan2(Math.sin(brng) * Math.sin(dist) * Math.cos(lat1), Math.cos(dist) + - Math.sin(lat1) * Math.sin(lat2)); + + final double lat2deg = lat2 / DEG2RAD; + final double lon2deg = lon2 / DEG2RAD; + + return new GeoPoint(lat2deg, lon2deg); + } + + public static GeoPoint fromCenterBetween(final GeoPoint geoPointA, final GeoPoint geoPointB) { + return new GeoPoint((geoPointA.getLatitudeE6() + geoPointB.getLatitudeE6()) / 2, + (geoPointA.getLongitudeE6() + geoPointB.getLongitudeE6()) / 2); + } + + public String toDoubleString() { + return new StringBuilder().append(this.mLatitudeE6 / 1E6).append(",") + .append(this.mLongitudeE6 / 1E6).append(",").append(this.mAltitude).toString(); + } + + public String toInvertedDoubleString() { + return new StringBuilder().append(this.mLongitudeE6 / 1E6).append(",") + .append(this.mLatitudeE6 / 1E6).append(",").append(this.mAltitude).toString(); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/util/GeometryMath.java b/src/main/java/org/osmdroid/util/GeometryMath.java new file mode 100644 index 000000000..9dd890dfd --- /dev/null +++ b/src/main/java/org/osmdroid/util/GeometryMath.java @@ -0,0 +1,63 @@ +package org.osmdroid.util; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * + * @author Marc Kurtz + * + */ +public class GeometryMath +{ + public static final double DEG2RAD = (Math.PI / 180.0); + public static final double RAD2DEG = (180.0 / Math.PI); + + public static final Rect getBoundingBoxForRotatatedRectangle(Rect rect, float angle, Rect reuse) { + return getBoundingBoxForRotatatedRectangle(rect, rect.centerX(), rect.centerY(), angle, + reuse); + } + + public static final Rect getBoundingBoxForRotatatedRectangle(Rect rect, Point centerPoint, + float angle, Rect reuse) { + return getBoundingBoxForRotatatedRectangle(rect, centerPoint.x, centerPoint.y, angle, reuse); + } + + public static final Rect getBoundingBoxForRotatatedRectangle(Rect rect, int centerX, + int centerY, float angle, Rect reuse) { + if (reuse == null) + reuse = new Rect(); + + double theta = angle * DEG2RAD; + double sinTheta = Math.sin(theta); + double cosTheta = Math.cos(theta); + double dx1 = rect.left - centerX; + double dy1 = rect.top - centerY; + double newX1 = centerX - dx1 * cosTheta + dy1 * sinTheta; + double newY1 = centerY - dx1 * sinTheta - dy1 * cosTheta; + double dx2 = rect.right - centerX; + double dy2 = rect.top - centerY; + double newX2 = centerX - dx2 * cosTheta + dy2 * sinTheta; + double newY2 = centerY - dx2 * sinTheta - dy2 * cosTheta; + double dx3 = rect.left - centerX; + double dy3 = rect.bottom - centerY; + double newX3 = centerX - dx3 * cosTheta + dy3 * sinTheta; + double newY3 = centerY - dx3 * sinTheta - dy3 * cosTheta; + double dx4 = rect.right - centerX; + double dy4 = rect.bottom - centerY; + double newX4 = centerX - dx4 * cosTheta + dy4 * sinTheta; + double newY4 = centerY - dx4 * sinTheta - dy4 * cosTheta; + reuse.set((int) Min4(newX1, newX2, newX3, newX4), (int) Min4(newY1, newY2, newY3, newY4), + (int) Max4(newX1, newX2, newX3, newX4), (int) Max4(newY1, newY2, newY3, newY4)); + + return reuse; + } + + private static double Min4(double a, double b, double c, double d) { + return Math.floor(Math.min(Math.min(a, b), Math.min(c, d))); + } + + private static double Max4(double a, double b, double c, double d) { + return Math.ceil(Math.max(Math.max(a, b), Math.max(c, d))); + } +} diff --git a/src/main/java/org/osmdroid/util/LocationUtils.java b/src/main/java/org/osmdroid/util/LocationUtils.java new file mode 100644 index 000000000..cbf4dba2f --- /dev/null +++ b/src/main/java/org/osmdroid/util/LocationUtils.java @@ -0,0 +1,48 @@ +package org.osmdroid.util; + +import org.osmdroid.util.constants.UtilConstants; + +import android.location.Location; +import android.location.LocationManager; + +public class LocationUtils implements UtilConstants { + + /** + * This is a utility class with only static members. + */ + private LocationUtils() { + } + + /** + * Get the most recent location from the GPS or Network provider. + * + * @return return the most recent location, or null if there's no known location + */ + public static Location getLastKnownLocation(final LocationManager pLocationManager) { + if (pLocationManager == null) { + return null; + } + final Location gpsLocation = getLastKnownLocation(pLocationManager, LocationManager.GPS_PROVIDER); + final Location networkLocation = getLastKnownLocation(pLocationManager, LocationManager.NETWORK_PROVIDER); + if (gpsLocation == null) { + return networkLocation; + } else if (networkLocation == null) { + return gpsLocation; + } else { + // both are non-null - use the most recent + if (networkLocation.getTime() > gpsLocation.getTime() + GPS_WAIT_TIME) { + return networkLocation; + } else { + return gpsLocation; + } + } + } + + private static Location getLastKnownLocation(final LocationManager pLocationManager, final String pProvider) { + if (!pLocationManager.isProviderEnabled(pProvider)) { + return null; + } + return pLocationManager.getLastKnownLocation(pProvider); + } + +} diff --git a/src/main/java/org/osmdroid/util/MyMath.java b/src/main/java/org/osmdroid/util/MyMath.java new file mode 100644 index 000000000..28cb3eaf9 --- /dev/null +++ b/src/main/java/org/osmdroid/util/MyMath.java @@ -0,0 +1,63 @@ +// Created by plusminus on 20:36:01 - 26.09.2008 +package org.osmdroid.util; + +import org.osmdroid.views.util.constants.MathConstants; + +/** + * + * @author Nicolas Gramlich + * + */ +public class MyMath implements MathConstants { + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + // =========================================================== + // Constructors + // =========================================================== + + /** + * This is a utility class with only static members. + */ + private MyMath() { + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + public static double gudermannInverse(final double aLatitude) { + return Math.log(Math.tan(PI_4 + (DEG2RAD * aLatitude / 2))); + } + + public static double gudermann(final double y) { + return RAD2DEG * Math.atan(Math.sinh(y)); + } + + public static int mod(int number, final int modulus) { + if (number > 0) + return number % modulus; + + while (number < 0) + number += modulus; + + return number; + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/util/NetworkLocationIgnorer.java b/src/main/java/org/osmdroid/util/NetworkLocationIgnorer.java new file mode 100644 index 000000000..c0e4d620c --- /dev/null +++ b/src/main/java/org/osmdroid/util/NetworkLocationIgnorer.java @@ -0,0 +1,42 @@ +package org.osmdroid.util; + +import org.osmdroid.util.constants.UtilConstants; + +import android.location.LocationManager; + +/** + * + * A class to check whether we want to use a location. If there are multiple location providers, + * i.e. network and GPS, then you want to ignore network locations shortly after a GPS location + * because you will get another GPS location soon. + * + * @author Neil Boyd + * + */ +public class NetworkLocationIgnorer implements UtilConstants { + + /** last time we got a location from the gps provider */ + private long mLastGps = 0; + + /** + * Whether we should ignore this location. + * + * @param pProvider + * the provider that provided the location + * @param pTime + * the time of the location + * @return true if we should ignore this location, false if not + */ + public boolean shouldIgnore(final String pProvider, final long pTime) { + + if (LocationManager.GPS_PROVIDER.equals(pProvider)) { + mLastGps = pTime; + } else { + if (pTime < mLastGps + GPS_WAIT_TIME) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/org/osmdroid/util/Position.java b/src/main/java/org/osmdroid/util/Position.java new file mode 100644 index 000000000..9618ad756 --- /dev/null +++ b/src/main/java/org/osmdroid/util/Position.java @@ -0,0 +1,57 @@ +package org.osmdroid.util; + +import org.osmdroid.api.IPosition; + +public class Position implements IPosition { + private final double mLatitude; + private final double mLongitude; + private boolean mHasBearing; + private float mBearing; + private boolean mHasZoomLevel; + private float mZoomLevel; + + public Position(final double aLatitude, final double aLongitude) { + mLatitude = aLatitude; + mLongitude = aLongitude; + } + + @Override + public double getLatitude() { + return mLatitude; + } + + @Override + public double getLongitude() { + return mLongitude; + } + + @Override + public boolean hasBearing() { + return mHasBearing; + } + + @Override + public float getBearing() { + return mBearing; + } + + public void setBearing(final float aBearing) { + mHasBearing = true; + mBearing = aBearing; + } + + @Override + public boolean hasZoomLevel() { + return mHasZoomLevel; + } + + @Override + public float getZoomLevel() { + return mZoomLevel; + } + + public void setZoomLevel(final float aZoomLevel) { + mHasZoomLevel = true; + mZoomLevel = aZoomLevel; + } +} diff --git a/src/main/java/org/osmdroid/util/ResourceProxyImpl.java b/src/main/java/org/osmdroid/util/ResourceProxyImpl.java new file mode 100644 index 000000000..3aaedec36 --- /dev/null +++ b/src/main/java/org/osmdroid/util/ResourceProxyImpl.java @@ -0,0 +1,54 @@ +package org.osmdroid.util; + +import java.lang.reflect.Field; + +import org.osmdroid.DefaultResourceProxyImpl; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; + +/** + * This is an extension of {@link org.osmdroid.DefaultResourceProxyImpl} + * that first tries to get from resources by using reflection to get the id of the resource + * from the package that the context is in. + */ +public class ResourceProxyImpl extends DefaultResourceProxyImpl { + + private final Resources mResources; + private final String mResourceNameBase; + + public ResourceProxyImpl(final Context aContext) { + super(aContext); + mResources = aContext.getResources(); + mResourceNameBase = aContext.getPackageName() + ".R$"; + } + + @Override + public String getString(final string pResId) { + final int id = getId("string", pResId.name()); + return id != 0 ? mResources.getString(id) : super.getString(pResId); + } + + @Override + public String getString(final string pResId, final Object... formatArgs) { + final int id = getId("string", pResId.name()); + return id != 0 ? mResources.getString(id, formatArgs) : super.getString(pResId, formatArgs); + } + + @Override + public Drawable getDrawable(final bitmap pResId) { + final int id = getId("drawable", pResId.name()); + return id != 0 ? mResources.getDrawable(id) : super.getDrawable(pResId); + } + + private int getId(final String aType, final String aName) { + try { + final Class cls = Class.forName(mResourceNameBase + aType); + final Field field = cls.getDeclaredField(aName); + return field.getInt(null); + } catch (final Exception e) { + return 0; + } + } +} diff --git a/src/main/java/org/osmdroid/util/TileLooper.java b/src/main/java/org/osmdroid/util/TileLooper.java new file mode 100644 index 000000000..378179a1b --- /dev/null +++ b/src/main/java/org/osmdroid/util/TileLooper.java @@ -0,0 +1,46 @@ +package org.osmdroid.util; + +import org.osmdroid.tileprovider.MapTile; + +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; + +/** + * A class that will loop around all the map tiles in the given viewport. + */ +public abstract class TileLooper { + + protected final Point mUpperLeft = new Point(); + protected final Point mLowerRight = new Point(); + + public final void loop(final Canvas pCanvas, final int pZoomLevel, final int pTileSizePx, final Rect pViewPort) { + // Calculate the amount of tiles needed for each side around the center one. + TileSystem.PixelXYToTileXY(pViewPort.left, pViewPort.top, mUpperLeft); + mUpperLeft.offset(-1, -1); + TileSystem.PixelXYToTileXY(pViewPort.right, pViewPort.bottom, mLowerRight); + + final int mapTileUpperBound = 1 << pZoomLevel; + + initialiseLoop(pZoomLevel, pTileSizePx); + + /* Draw all the MapTiles (from the upper left to the lower right). */ + for (int y = mUpperLeft.y; y <= mLowerRight.y; y++) { + for (int x = mUpperLeft.x; x <= mLowerRight.x; x++) { + // Construct a MapTile to request from the tile provider. + final int tileY = MyMath.mod(y, mapTileUpperBound); + final int tileX = MyMath.mod(x, mapTileUpperBound); + final MapTile tile = new MapTile(pZoomLevel, tileX, tileY); + handleTile(pCanvas, pTileSizePx, tile, x, y); + } + } + + finaliseLoop(); + } + + public abstract void initialiseLoop(int pZoomLevel, int pTileSizePx); + + public abstract void handleTile(Canvas pCanvas, int pTileSizePx, MapTile pTile, int pX, int pY); + + public abstract void finaliseLoop(); +} diff --git a/src/main/java/org/osmdroid/util/TileSystem.java b/src/main/java/org/osmdroid/util/TileSystem.java new file mode 100644 index 000000000..10412d3ec --- /dev/null +++ b/src/main/java/org/osmdroid/util/TileSystem.java @@ -0,0 +1,115 @@ +package org.osmdroid.util; + +import android.graphics.Point; + +/** + * Proxy class for TileSystem. For coordinate conversions (tile to lat/lon and reverse) TileSystem + * only accepts input parameters within certain ranges and crops any values outside of it. For + * lat/lon the range is ~(-85,+85) / (-180,+180) and for tile coordinates (0,mapsize-1). Under + * certain conditions osmdroid creates values outside of these ranges, for example when zooming out + * and displaying the earth more that once side by side or when scrolling across the 180 degree + * longitude (international date line). This class fixes this by wrapping input coordinates into a + * valid range by adding/subtracting the valid span. Example: longitude +185 => -175 + * + * @author Oliver Seiler + */ +public final class TileSystem { + + /** @see microsoft.mappoint.TileSystem#setTileSize(int) */ + public static void setTileSize(final int tileSize) { + microsoft.mappoint.TileSystem.setTileSize(tileSize); + } + + /** @see microsoft.mappoint.TileSystem#getTileSize() */ + public static int getTileSize() { + return microsoft.mappoint.TileSystem.getTileSize(); + } + + /** @see microsoft.mappoint.TileSystem#MapSize(int) */ + public static int MapSize(final int levelOfDetail) { + return microsoft.mappoint.TileSystem.MapSize(levelOfDetail); + } + + /** @see microsoft.mappoint.TileSystem#GroundResolution(double, int) */ + public static double GroundResolution(final double latitude, final int levelOfDetail) { + return microsoft.mappoint.TileSystem.GroundResolution(wrap(latitude, -90, 90, 180), levelOfDetail); + } + + /** @see microsoft.mappoint.TileSystem#MapScale(double, int, int) */ + public static double MapScale(final double latitude, final int levelOfDetail, final int screenDpi) { + return microsoft.mappoint.TileSystem.MapScale(latitude, levelOfDetail, screenDpi); + } + + /** @see microsoft.mappoint.TileSystem#LatLongToPixelXY(double, double, int, Point) */ + public static Point LatLongToPixelXY( + final double latitude, final double longitude, final int levelOfDetail, final Point reuse) { + return microsoft.mappoint.TileSystem.LatLongToPixelXY( + wrap(latitude, -90, 90, 180), + wrap(longitude, -180, 180, 360), + levelOfDetail, reuse); + } + + /** @see microsoft.mappoint.TileSystem#PixelXYToLatLong(int, int, int, GeoPoint) */ + public static GeoPoint PixelXYToLatLong( + final int pixelX, final int pixelY, final int levelOfDetail, final GeoPoint reuse) { + final int mapSize = MapSize(levelOfDetail); + return microsoft.mappoint.TileSystem.PixelXYToLatLong( + (int) wrap(pixelX, 0, mapSize - 1, mapSize), + (int) wrap(pixelY, 0, mapSize - 1, mapSize), + levelOfDetail, reuse); + } + + /** @see microsoft.mappoint.TileSystem#PixelXYToTileXY(int, int, Point) */ + public static Point PixelXYToTileXY(final int pixelX, final int pixelY, final Point reuse) { + return microsoft.mappoint.TileSystem.PixelXYToTileXY(pixelX, pixelY, reuse); + } + + /** @see microsoft.mappoint.TileSystem#TileXYToPixelXY(int, int, Point) */ + public static Point TileXYToPixelXY(final int tileX, final int tileY, final Point reuse) { + return microsoft.mappoint.TileSystem.TileXYToPixelXY(tileX, tileY, reuse); + } + + /** @see microsoft.mappoint.TileSystem#TileXYToQuadKey(int, int, int) */ + public static String TileXYToQuadKey(final int tileX, final int tileY, final int levelOfDetail) { + return microsoft.mappoint.TileSystem.TileXYToQuadKey(tileX, tileY, levelOfDetail); + } + + /** @see microsoft.mappoint.TileSystem#QuadKeyToTileXY(String, Point) */ + public static Point QuadKeyToTileXY(final String quadKey, final Point reuse) { + return microsoft.mappoint.TileSystem.QuadKeyToTileXY(quadKey, reuse); + } + + /** + * Returns a value that lies within minValue and maxValue by + * subtracting/adding interval. + * + * @param n + * the input number + * @param minValue + * the minimum value + * @param maxValue + * the maximum value + * @param interval + * the interval length + * @return a value that lies within minValue and maxValue by + * subtracting/adding interval + */ + private static double wrap(double n, final double minValue, final double maxValue, final double interval) { + if (minValue > maxValue) { + throw new IllegalArgumentException("minValue must be smaller than maxValue: " + + minValue + ">" + maxValue); + } + if (interval > maxValue - minValue + 1) { + throw new IllegalArgumentException( + "interval must be equal or smaller than maxValue-minValue: " + "min: " + + minValue + " max:" + maxValue + " int:" + interval); + } + while (n < minValue) { + n += interval; + } + while (n > maxValue) { + n -= interval; + } + return n; + } +} diff --git a/src/main/java/org/osmdroid/util/constants/GeoConstants.java b/src/main/java/org/osmdroid/util/constants/GeoConstants.java new file mode 100644 index 000000000..2272e86c3 --- /dev/null +++ b/src/main/java/org/osmdroid/util/constants/GeoConstants.java @@ -0,0 +1,18 @@ +// Created by plusminus on 17:41:55 - 16.10.2008 +package org.osmdroid.util.constants; + +public interface GeoConstants { + // =========================================================== + // Final Fields + // =========================================================== + + public static final int RADIUS_EARTH_METERS = 6378137; // http://en.wikipedia.org/wiki/Earth_radius#Equatorial_radius + public static final double METERS_PER_STATUTE_MILE = 1609.344; // http://en.wikipedia.org/wiki/Mile + public static final double METERS_PER_NAUTICAL_MILE = 1852; // http://en.wikipedia.org/wiki/Nautical_mile + public static final double FEET_PER_METER = 3.2808399; // http://en.wikipedia.org/wiki/Feet_%28unit_of_length%29 + public static final int EQUATORCIRCUMFENCE = (int) (2 * Math.PI * RADIUS_EARTH_METERS); + + // =========================================================== + // Methods + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/util/constants/UtilConstants.java b/src/main/java/org/osmdroid/util/constants/UtilConstants.java new file mode 100644 index 000000000..56e22f429 --- /dev/null +++ b/src/main/java/org/osmdroid/util/constants/UtilConstants.java @@ -0,0 +1,10 @@ +package org.osmdroid.util.constants; + +public interface UtilConstants { + + /** + * The time we wait after the last gps location before using a non-gps location. + */ + public static final long GPS_WAIT_TIME = 20000; // 20 seconds + +} diff --git a/src/main/java/org/osmdroid/views/MapController.java b/src/main/java/org/osmdroid/views/MapController.java new file mode 100644 index 000000000..f978278a8 --- /dev/null +++ b/src/main/java/org/osmdroid/views/MapController.java @@ -0,0 +1,310 @@ +// Created by plusminus on 21:37:08 - 27.09.2008 +package org.osmdroid.views; + +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.api.IMapController; +import org.osmdroid.util.BoundingBoxE6; +import org.osmdroid.views.util.MyMath; +import org.osmdroid.views.util.constants.MapViewConstants; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Build; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.ScaleAnimation; + +/** + * + * @author Nicolas Gramlich + * @author Marc Kurtz + */ +public class MapController implements IMapController, MapViewConstants { + + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected final MapView mMapView; + + // Zoom animations + private ValueAnimator mZoomInAnimation; + private ValueAnimator mZoomOutAnimation; + private ScaleAnimation mZoomInAnimationOld; + private ScaleAnimation mZoomOutAnimationOld; + + private Animator mCurrentAnimator; + + // =========================================================== + // Constructors + // =========================================================== + + public MapController(MapView mapView) { + mMapView = mapView; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mZoomInAnimation = ValueAnimator.ofFloat(1f, 2f); + mZoomInAnimation.addListener(new MyZoomAnimatorListener()); + mZoomInAnimation.addUpdateListener(new MyZoomAnimatorUpdateListener()); + mZoomInAnimation.setDuration(ANIMATION_DURATION_SHORT); + + mZoomOutAnimation = ValueAnimator.ofFloat(1f, 0.5f); + mZoomOutAnimation.addListener(new MyZoomAnimatorListener()); + mZoomOutAnimation.addUpdateListener(new MyZoomAnimatorUpdateListener()); + mZoomOutAnimation.setDuration(ANIMATION_DURATION_SHORT); + } else { + mZoomInAnimationOld = new ScaleAnimation(1, 2, 1, 2, Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f); + mZoomOutAnimationOld = new ScaleAnimation(1, 0.5f, 1, 0.5f, Animation.RELATIVE_TO_SELF, + 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + mZoomInAnimationOld.setDuration(ANIMATION_DURATION_SHORT); + mZoomOutAnimationOld.setDuration(ANIMATION_DURATION_SHORT); + mZoomInAnimationOld.setAnimationListener(new MyZoomAnimationListener()); + mZoomOutAnimationOld.setAnimationListener(new MyZoomAnimationListener()); + } + } + + public void zoomToSpan(final BoundingBoxE6 bb) { + zoomToSpan(bb.getLatitudeSpanE6(), bb.getLongitudeSpanE6()); + } + + // TODO rework zoomToSpan + @Override + public void zoomToSpan(int latSpanE6, int lonSpanE6) { + if (latSpanE6 <= 0 || lonSpanE6 <= 0) { + return; + } + + final BoundingBoxE6 bb = this.mMapView.getBoundingBox(); + final int curZoomLevel = this.mMapView.getZoomLevel(); + + final int curLatSpan = bb.getLatitudeSpanE6(); + final int curLonSpan = bb.getLongitudeSpanE6(); + + final float diffNeededLat = (float) latSpanE6 / curLatSpan; // i.e. 600/500 = 1,2 + final float diffNeededLon = (float) lonSpanE6 / curLonSpan; // i.e. 300/400 = 0,75 + + final float diffNeeded = Math.max(diffNeededLat, diffNeededLon); // i.e. 1,2 + + if (diffNeeded > 1) { // Zoom Out + this.mMapView.setZoomLevel(curZoomLevel - MyMath.getNextSquareNumberAbove(diffNeeded)); + } else if (diffNeeded < 0.5) { // Can Zoom in + this.mMapView.setZoomLevel(curZoomLevel + + MyMath.getNextSquareNumberAbove(1 / diffNeeded) - 1); + } + } + + /** + * Start animating the map towards the given point. + */ + @Override + public void animateTo(final IGeoPoint point) { + Point p = mMapView.getProjection().toMapPixels(point, null); + animateTo(p.x, p.y); + } + + /** + * Start animating the map towards the given point. + */ + public void animateTo(int x, int y) { + if (!mMapView.isAnimating()) { + mMapView.mIsFlinging = false; + final int xStart = mMapView.getScrollX(); + final int yStart = mMapView.getScrollY(); + mMapView.getScroller().startScroll(xStart, yStart, x - xStart, y - yStart, + ANIMATION_DURATION_DEFAULT); + mMapView.postInvalidate(); + } + } + + @Override + public void scrollBy(int x, int y) { + this.mMapView.scrollBy(x, y); + } + + /** + * Set the map view to the given center. There will be no animation. + */ + @Override + public void setCenter(final IGeoPoint point) { + Point p = mMapView.getProjection().toMapPixels(point, null); + this.mMapView.scrollTo(p.x, p.y); + } + + @Override + public void stopPanning() { + mMapView.mIsFlinging = false; + mMapView.getScroller().forceFinished(true); + } + + /** + * Stops a running animation. + * + * @param jumpToTarget + */ + @Override + public void stopAnimation(final boolean jumpToTarget) { + + if (!mMapView.getScroller().isFinished()) { + if (jumpToTarget) { + mMapView.mIsFlinging = false; + mMapView.getScroller().abortAnimation(); + } else + stopPanning(); + } + + // We ignore the jumpToTarget for zoom levels since it doesn't make sense to stop + // the animation in the middle. Maybe we could have it cancel the zoom operation and jump + // back to original zoom level? + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + final Animator currentAnimator = this.mCurrentAnimator; + if (mMapView.mIsAnimating.get()) { + currentAnimator.end(); + } + } else { + if (mMapView.mIsAnimating.get()) { + mMapView.clearAnimation(); + } + } + } + + @Override + public int setZoom(final int zoomlevel) { + return mMapView.setZoomLevel(zoomlevel); + } + + /** + * Zoom in by one zoom level. + */ + @Override + public boolean zoomIn() { + Point coords = mMapView.getProjection().toMapPixels(mMapView.getMapCenter(), null); + return zoomInFixing(coords.x, coords.y); + } + + @Override + public boolean zoomInFixing(final int xPixel, final int yPixel) { + mMapView.mMultiTouchScalePoint.set(xPixel, yPixel); + if (mMapView.canZoomIn()) { + if (mMapView.mIsAnimating.getAndSet(true)) { + // TODO extend zoom (and return true) + return false; + } else { + mMapView.mTargetZoomLevel.set(mMapView.getZoomLevel(false) + 1); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mCurrentAnimator = mZoomInAnimation; + mZoomInAnimation.start(); + } else { + mMapView.startAnimation(mZoomInAnimationOld); + } + return true; + } + } else { + return false; + } + } + + /** + * Zoom out by one zoom level. + */ + @Override + public boolean zoomOut() { + Point coords = mMapView.getProjection().toMapPixels(mMapView.getMapCenter(), null); + return zoomOutFixing(coords.x, coords.y); + } + + @Override + public boolean zoomOutFixing(final int xPixel, final int yPixel) { + mMapView.mMultiTouchScalePoint.set(xPixel, yPixel); + if (mMapView.canZoomOut()) { + if (mMapView.mIsAnimating.getAndSet(true)) { + // TODO extend zoom (and return true) + return false; + } else { + mMapView.mTargetZoomLevel.set(mMapView.getZoomLevel(false) - 1); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mCurrentAnimator = mZoomOutAnimation; + mZoomOutAnimation.start(); + } else { + mMapView.startAnimation(mZoomOutAnimationOld); + } + return true; + } + } else { + return false; + } + } + + protected void onAnimationStart() { + mMapView.mIsAnimating.set(true); + } + + protected void onAnimationEnd() { + final Rect screenRect = mMapView.getProjection().getScreenRect(); + final Matrix m = new Matrix(); + m.setScale(1 / mMapView.mMultiTouchScale, 1 / mMapView.mMultiTouchScale, + mMapView.mMultiTouchScalePoint.x, mMapView.mMultiTouchScalePoint.y); + m.postRotate(-mMapView.getMapOrientation(), screenRect.exactCenterX(), + screenRect.exactCenterY()); + float[] pts = new float[2]; + pts[0] = mMapView.getScrollX(); + pts[1] = mMapView.getScrollY(); + m.mapPoints(pts); + mMapView.scrollTo((int) pts[0], (int) pts[1]); + setZoom(mMapView.mTargetZoomLevel.get()); + mMapView.mMultiTouchScale = 1f; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mCurrentAnimator = null; + } + mMapView.mIsAnimating.set(false); + } + + protected class MyZoomAnimatorListener extends AnimatorListenerAdapter { + @Override + public void onAnimationStart(Animator animation) { + MapController.this.onAnimationStart(); + super.onAnimationStart(animation); + } + + @Override + public void onAnimationEnd(Animator animation) { + MapController.this.onAnimationEnd(); + super.onAnimationEnd(animation); + } + } + + protected class MyZoomAnimatorUpdateListener implements AnimatorUpdateListener { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mMapView.mMultiTouchScale = (Float) animation.getAnimatedValue(); + mMapView.invalidate(); + } + } + + protected class MyZoomAnimationListener implements AnimationListener { + + @Override + public void onAnimationStart(Animation animation) { + MapController.this.onAnimationStart(); + } + + @Override + public void onAnimationEnd(Animation animation) { + MapController.this.onAnimationEnd(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + // Nothing to do here... + } + } +} diff --git a/src/main/java/org/osmdroid/views/MapControllerOld.java b/src/main/java/org/osmdroid/views/MapControllerOld.java new file mode 100644 index 000000000..47107e077 --- /dev/null +++ b/src/main/java/org/osmdroid/views/MapControllerOld.java @@ -0,0 +1,685 @@ +// Created by plusminus on 21:37:08 - 27.09.2008 +package org.osmdroid.views; + +import microsoft.mappoint.TileSystem; + +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.api.IMapController; +import org.osmdroid.util.BoundingBoxE6; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.util.MyMath; +import org.osmdroid.views.util.constants.MapViewConstants; +import org.osmdroid.views.util.constants.MathConstants; + +import android.graphics.Point; + +/** + * + * @author Nicolas Gramlich + * + * @deprecated Use MapController instead. + */ +public class MapControllerOld implements IMapController, MapViewConstants { + + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + private final MapView mOsmv; + private AbstractAnimationRunner mCurrentAnimationRunner; + + // =========================================================== + // Constructors + // =========================================================== + + public MapControllerOld(final MapView osmv) { + this.mOsmv = osmv; + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + public void zoomToSpan(final BoundingBoxE6 bb) { + zoomToSpan(bb.getLatitudeSpanE6(), bb.getLongitudeSpanE6()); + } + + // TODO rework zoomToSpan + @Override + public void zoomToSpan(final int reqLatSpan, final int reqLonSpan) { + if (reqLatSpan <= 0 || reqLonSpan <= 0) { + return; + } + + final BoundingBoxE6 bb = this.mOsmv.getBoundingBox(); + final int curZoomLevel = this.mOsmv.getZoomLevel(); + + final int curLatSpan = bb.getLatitudeSpanE6(); + final int curLonSpan = bb.getLongitudeSpanE6(); + + final float diffNeededLat = (float) reqLatSpan / curLatSpan; // i.e. 600/500 = 1,2 + final float diffNeededLon = (float) reqLonSpan / curLonSpan; // i.e. 300/400 = 0,75 + + final float diffNeeded = Math.max(diffNeededLat, diffNeededLon); // i.e. 1,2 + + if (diffNeeded > 1) { // Zoom Out + this.mOsmv.setZoomLevel(curZoomLevel - MyMath.getNextSquareNumberAbove(diffNeeded)); + } else if (diffNeeded < 0.5) { // Can Zoom in + this.mOsmv.setZoomLevel(curZoomLevel + MyMath.getNextSquareNumberAbove(1 / diffNeeded) + - 1); + } + } + + /** + * Start animating the map towards the given point. + */ + @Override + public void animateTo(final IGeoPoint point) { + animateTo(point.getLatitudeE6() / 1E6, point.getLongitudeE6() / 1E6); + } + + /** + * Start animating the map towards the given point. + */ + public void animateTo(final double latitude, final double longitude) { + final int x = mOsmv.getScrollX(); + final int y = mOsmv.getScrollY(); + final Point p = TileSystem.LatLongToPixelXY(latitude, longitude, mOsmv.getZoomLevel(), null); + final int worldSize_2 = TileSystem.MapSize(mOsmv.getZoomLevel()) / 2; + mOsmv.getScroller().startScroll(x, y, p.x - worldSize_2 - x, p.y - worldSize_2 - y, + ANIMATION_DURATION_DEFAULT); + mOsmv.postInvalidate(); + } + + /** + * Animates the underlying {@link MapView} that it centers the passed {@link GeoPoint} in the + * end. Uses: {@link MapControllerOld.ANIMATION_SMOOTHNESS_DEFAULT} and + * {@link MapControllerOld.ANIMATION_DURATION_DEFAULT}. + * + * @param gp + */ + public void animateTo(final GeoPoint gp, final AnimationType aAnimationType) { + animateTo(gp.getLatitudeE6(), gp.getLongitudeE6(), aAnimationType, + ANIMATION_DURATION_DEFAULT, ANIMATION_SMOOTHNESS_DEFAULT); + } + + /** + * Animates the underlying {@link MapView} that it centers the passed {@link GeoPoint} in the + * end. + * + * @param gp + * GeoPoint to be centered in the end. + * @param aSmoothness + * steps made during animation. I.e.: {@link MapControllerOld.ANIMATION_SMOOTHNESS_LOW}, + * {@link MapControllerOld.ANIMATION_SMOOTHNESS_DEFAULT}, + * {@link MapControllerOld.ANIMATION_SMOOTHNESS_HIGH} + * @param aDuration + * in Milliseconds. I.e.: {@link MapControllerOld.ANIMATION_DURATION_SHORT}, + * {@link MapControllerOld.ANIMATION_DURATION_DEFAULT}, + * {@link MapControllerOld.ANIMATION_DURATION_LONG} + */ + public void animateTo(final GeoPoint gp, final AnimationType aAnimationType, + final int aSmoothness, final int aDuration) { + animateTo(gp.getLatitudeE6(), gp.getLongitudeE6(), aAnimationType, aSmoothness, aDuration); + } + + /** + * Animates the underlying {@link MapView} that it centers the passed coordinates in the end. + * Uses: {@link MapControllerOld.ANIMATION_SMOOTHNESS_DEFAULT} and + * {@link MapControllerOld.ANIMATION_DURATION_DEFAULT}. + * + * @param aLatitudeE6 + * @param aLongitudeE6 + */ + public void animateTo(final int aLatitudeE6, final int aLongitudeE6, + final AnimationType aAnimationType) { + animateTo(aLatitudeE6, aLongitudeE6, aAnimationType, ANIMATION_SMOOTHNESS_DEFAULT, + ANIMATION_DURATION_DEFAULT); + } + + /** + * Animates the underlying {@link MapView} that it centers the passed coordinates in the end. + * + * @param aLatitudeE6 + * @param aLongitudeE6 + * @param aSmoothness + * steps made during animation. I.e.: {@link MapControllerOld.ANIMATION_SMOOTHNESS_LOW}, + * {@link MapControllerOld.ANIMATION_SMOOTHNESS_DEFAULT}, + * {@link MapControllerOld.ANIMATION_SMOOTHNESS_HIGH} + * @param aDuration + * in Milliseconds. I.e.: {@link MapControllerOld.ANIMATION_DURATION_SHORT}, + * {@link MapControllerOld.ANIMATION_DURATION_DEFAULT}, + * {@link MapControllerOld.ANIMATION_DURATION_LONG} + */ + public void animateTo(final int aLatitudeE6, final int aLongitudeE6, + final AnimationType aAnimationType, final int aSmoothness, final int aDuration) { + this.stopAnimation(false); + + switch (aAnimationType) { + case LINEAR: + this.mCurrentAnimationRunner = new LinearAnimationRunner(aLatitudeE6, aLongitudeE6, + aSmoothness, aDuration); + break; + case EXPONENTIALDECELERATING: + this.mCurrentAnimationRunner = new ExponentialDeceleratingAnimationRunner(aLatitudeE6, + aLongitudeE6, aSmoothness, aDuration); + break; + case QUARTERCOSINUSALDECELERATING: + this.mCurrentAnimationRunner = new QuarterCosinusalDeceleratingAnimationRunner( + aLatitudeE6, aLongitudeE6, aSmoothness, aDuration); + break; + case HALFCOSINUSALDECELERATING: + this.mCurrentAnimationRunner = new HalfCosinusalDeceleratingAnimationRunner( + aLatitudeE6, aLongitudeE6, aSmoothness, aDuration); + break; + case MIDDLEPEAKSPEED: + this.mCurrentAnimationRunner = new MiddlePeakSpeedAnimationRunner(aLatitudeE6, + aLongitudeE6, aSmoothness, aDuration); + break; + } + + this.mCurrentAnimationRunner.start(); + } + + public void scrollBy(final int x, final int y) { + this.mOsmv.scrollBy(x, y); + } + + /** + * Set the map view to the given center. There will be no animation. + */ + @Override + public void setCenter(final IGeoPoint point) { + final Point p = TileSystem.LatLongToPixelXY(point.getLatitudeE6() / 1E6, + point.getLongitudeE6() / 1E6, this.mOsmv.getZoomLevel(), null); + final int worldSize_2 = TileSystem.MapSize(this.mOsmv.getZoomLevel()) / 2; + this.mOsmv.scrollTo(p.x - worldSize_2, p.y - worldSize_2); + } + + /** + * Stops a running animation. + * + * @param jumpToTarget + */ + public void stopAnimation(final boolean jumpToTarget) { + final AbstractAnimationRunner currentAnimationRunner = this.mCurrentAnimationRunner; + + if (currentAnimationRunner != null && !currentAnimationRunner.isDone()) { + currentAnimationRunner.interrupt(); + if (jumpToTarget) { + setCenter(new GeoPoint(currentAnimationRunner.mTargetLatitudeE6, + currentAnimationRunner.mTargetLongitudeE6)); + } + } + } + + @Override + public void stopPanning() { + mOsmv.getScroller().forceFinished(true); + } + + @Override + public int setZoom(final int zoomlevel) { + return mOsmv.setZoomLevel(zoomlevel); + } + + /** + * Zoom in by one zoom level. + */ + @Override + public boolean zoomIn() { + return mOsmv.zoomIn(); + } + + public boolean zoomInFixing(final GeoPoint point) { + return mOsmv.zoomInFixing(point); + } + + @Override + public boolean zoomInFixing(final int xPixel, final int yPixel) { + return mOsmv.zoomInFixing(xPixel, yPixel); + } + + /** + * Zoom out by one zoom level. + */ + @Override + public boolean zoomOut() { + return mOsmv.zoomOut(); + } + + public boolean zoomOutFixing(final GeoPoint point) { + return mOsmv.zoomOutFixing(point); + } + + @Override + public boolean zoomOutFixing(final int xPixel, final int yPixel) { + return mOsmv.zoomOutFixing(xPixel, yPixel); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + /** + * Choose on of the Styles of approacing the target Coordinates. + *
    + *
  • LINEAR + *
      + *
    • Uses ses linear interpolation
    • + *
    • Values produced: 10%, 20%, 30%, 40%, 50%, ...
    • + *
    • Style: Always average speed.
    • + *
    + *
  • + *
  • EXPONENTIALDECELERATING + *
      + *
    • Uses a exponential interpolation/li> + *
    • Values produced: 50%, 75%, 87.5%, 93.5%, ...
    • + *
    • Style: Starts very fast, really slow in the end.
    • + *
    + *
  • + *
  • QUARTERCOSINUSALDECELERATING + *
      + *
    • Uses the first quarter of the cos curve (from zero to PI/2) for interpolation.
    • + *
    • Values produced: See cos curve :)
    • + *
    • Style: Average speed, slows out medium.
    • + *
    + *
  • + *
  • HALFCOSINUSALDECELERATING + *
      + *
    • Uses the first half of the cos curve (from zero to PI) for interpolation
    • + *
    • Values produced: See cos curve :)
    • + *
    • Style: Average speed, slows out smoothly.
    • + *
    + *
  • + *
  • MIDDLEPEAKSPEED + *
      + *
    • Uses the values of cos around the 0 (from -PI/2 to +PI/2) for interpolation
    • + *
    • Values produced: See cos curve :)
    • + *
    • Style: Starts medium, speeds high in middle, slows out medium.
    • + *
    + *
  • + *
+ */ + public static enum AnimationType { + /** + *
    + *
  • LINEAR + *
      + *
    • Uses ses linear interpolation
    • + *
    • Values produced: 10%, 20%, 30%, 40%, 50%, ...
    • + *
    • Style: Always average speed.
    • + *
    + *
  • + *
+ */ + LINEAR, + /** + *
    + *
  • EXPONENTIALDECELERATING + *
      + *
    • Uses a exponential interpolation/li> + *
    • Values produced: 50%, 75%, 87.5%, 93.5%, ...
    • + *
    • Style: Starts very fast, really slow in the end.
    • + *
    + *
  • + *
+ */ + EXPONENTIALDECELERATING, + /** + *
    + *
  • QUARTERCOSINUSALDECELERATING + *
      + *
    • Uses the first quarter of the cos curve (from zero to PI/2) for interpolation.
    • + *
    • Values produced: See cos curve :)
    • + *
    • Style: Average speed, slows out medium.
    • + *
    + *
  • + *
+ */ + QUARTERCOSINUSALDECELERATING, + /** + *
    + *
  • HALFCOSINUSALDECELERATING + *
      + *
    • Uses the first half of the cos curve (from zero to PI) for interpolation
    • + *
    • Values produced: See cos curve :)
    • + *
    • Style: Average speed, slows out smoothly.
    • + *
    + *
  • + *
+ */ + HALFCOSINUSALDECELERATING, + /** + *
    + *
  • MIDDLEPEAKSPEED + *
      + *
    • Uses the values of cos around the 0 (from -PI/2 to +PI/2) for interpolation
    • + *
    • Values produced: See cos curve :)
    • + *
    • Style: Starts medium, speeds high in middle, slows out medium.
    • + *
    + *
  • + *
+ */ + MIDDLEPEAKSPEED + } + + /** + * @deprecated Do not use - this appears to modify UI elements and MapView fields on a + * background thread and is not thread-safe. + */ + private abstract class AbstractAnimationRunner extends Thread { + + // =========================================================== + // Fields + // =========================================================== + + protected final int mSmoothness; + protected final int mTargetLatitudeE6, mTargetLongitudeE6; + protected boolean mDone = false; + + protected final int mStepDuration; + + protected final int mPanTotalLatitudeE6, mPanTotalLongitudeE6; + + // =========================================================== + // Constructors + // =========================================================== + + @SuppressWarnings("unused") + public AbstractAnimationRunner(final MapControllerOld mapViewController, + final int aTargetLatitudeE6, final int aTargetLongitudeE6) { + this(aTargetLatitudeE6, aTargetLongitudeE6, + MapViewConstants.ANIMATION_SMOOTHNESS_DEFAULT, + MapViewConstants.ANIMATION_DURATION_DEFAULT); + } + + public AbstractAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, + final int aSmoothness, final int aDuration) { + this.mTargetLatitudeE6 = aTargetLatitudeE6; + this.mTargetLongitudeE6 = aTargetLongitudeE6; + this.mSmoothness = aSmoothness; + this.mStepDuration = aDuration / aSmoothness; + + /* Get the current mapview-center. */ + final MapView mapview = MapControllerOld.this.mOsmv; + final IGeoPoint mapCenter = mapview.getMapCenter(); + + this.mPanTotalLatitudeE6 = mapCenter.getLatitudeE6() - aTargetLatitudeE6; + this.mPanTotalLongitudeE6 = mapCenter.getLongitudeE6() - aTargetLongitudeE6; + } + + @Override + public void run() { + onRunAnimation(); + this.mDone = true; + } + + public boolean isDone() { + return this.mDone; + } + + public abstract void onRunAnimation(); + } + + private class LinearAnimationRunner extends AbstractAnimationRunner { + + // =========================================================== + // Fields + // =========================================================== + + protected final int mPanPerStepLatitudeE6, mPanPerStepLongitudeE6; + + // =========================================================== + // Constructors + // =========================================================== + + @SuppressWarnings("unused") + public LinearAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6) { + this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, + ANIMATION_DURATION_DEFAULT); + } + + public LinearAnimationRunner(final int aTargetLatitudeE6, final int aTargetLongitudeE6, + final int aSmoothness, final int aDuration) { + super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration); + + /* Get the current mapview-center. */ + final MapView mapview = MapControllerOld.this.mOsmv; + final IGeoPoint mapCenter = mapview.getMapCenter(); + + this.mPanPerStepLatitudeE6 = (mapCenter.getLatitudeE6() - aTargetLatitudeE6) + / aSmoothness; + this.mPanPerStepLongitudeE6 = (mapCenter.getLongitudeE6() - aTargetLongitudeE6) + / aSmoothness; + + this.setName("LinearAnimationRunner"); + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public void onRunAnimation() { + final MapView mapview = MapControllerOld.this.mOsmv; + final IGeoPoint mapCenter = mapview.getMapCenter(); + final int panPerStepLatitudeE6 = this.mPanPerStepLatitudeE6; + final int panPerStepLongitudeE6 = this.mPanPerStepLongitudeE6; + final int stepDuration = this.mStepDuration; + try { + int newMapCenterLatE6; + int newMapCenterLonE6; + + for (int i = this.mSmoothness; i > 0; i--) { + + newMapCenterLatE6 = mapCenter.getLatitudeE6() - panPerStepLatitudeE6; + newMapCenterLonE6 = mapCenter.getLongitudeE6() - panPerStepLongitudeE6; + mapview.setMapCenter(new GeoPoint(newMapCenterLatE6, newMapCenterLonE6)); + + Thread.sleep(stepDuration); + } + } catch (final Exception e) { + this.interrupt(); + } + } + } + + private class ExponentialDeceleratingAnimationRunner extends AbstractAnimationRunner { + + // =========================================================== + // Fields + // =========================================================== + + // =========================================================== + // Constructors + // =========================================================== + + @SuppressWarnings("unused") + public ExponentialDeceleratingAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6) { + this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, + ANIMATION_DURATION_DEFAULT); + } + + public ExponentialDeceleratingAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6, final int aSmoothness, final int aDuration) { + super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration); + + this.setName("ExponentialDeceleratingAnimationRunner"); + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public void onRunAnimation() { + final MapView mapview = MapControllerOld.this.mOsmv; + final IGeoPoint mapCenter = mapview.getMapCenter(); + final int stepDuration = this.mStepDuration; + try { + int newMapCenterLatE6; + int newMapCenterLonE6; + + for (int i = 0; i < this.mSmoothness; i++) { + + final double delta = Math.pow(0.5, i + 1); + final int deltaLatitudeE6 = (int) (this.mPanTotalLatitudeE6 * delta); + final int detlaLongitudeE6 = (int) (this.mPanTotalLongitudeE6 * delta); + + newMapCenterLatE6 = mapCenter.getLatitudeE6() - deltaLatitudeE6; + newMapCenterLonE6 = mapCenter.getLongitudeE6() - detlaLongitudeE6; + mapview.setMapCenter(new GeoPoint(newMapCenterLatE6, newMapCenterLonE6)); + + Thread.sleep(stepDuration); + } + mapview.setMapCenter(new GeoPoint(super.mTargetLatitudeE6, super.mTargetLongitudeE6)); + } catch (final Exception e) { + this.interrupt(); + } + } + } + + private class CosinusalBasedAnimationRunner extends AbstractAnimationRunner implements + MathConstants { + // =========================================================== + // Fields + // =========================================================== + + protected final float mStepIncrement, mAmountStretch; + protected final float mYOffset, mStart; + + // =========================================================== + // Constructors + // =========================================================== + + @SuppressWarnings("unused") + public CosinusalBasedAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6, final float aStart, final float aRange, + final float aYOffset) { + this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, + ANIMATION_DURATION_DEFAULT, aStart, aRange, aYOffset); + } + + public CosinusalBasedAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6, final int aSmoothness, final int aDuration, + final float aStart, final float aRange, final float aYOffset) { + super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration); + this.mYOffset = aYOffset; + this.mStart = aStart; + + this.mStepIncrement = aRange / aSmoothness; + + /* We need to normalize the amount in the end, so wee need the the: sum^(-1) . */ + float amountSum = 0; + for (int i = 0; i < aSmoothness; i++) { + amountSum += aYOffset + Math.cos(this.mStepIncrement * i + aStart); + } + + this.mAmountStretch = 1 / amountSum; + + this.setName("QuarterCosinusalDeceleratingAnimationRunner"); + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public void onRunAnimation() { + final MapView mapview = MapControllerOld.this.mOsmv; + final IGeoPoint mapCenter = mapview.getMapCenter(); + final int stepDuration = this.mStepDuration; + final float amountStretch = this.mAmountStretch; + try { + int newMapCenterLatE6; + int newMapCenterLonE6; + + for (int i = 0; i < this.mSmoothness; i++) { + + final double delta = (this.mYOffset + Math.cos(this.mStepIncrement * i + + this.mStart)) + * amountStretch; + final int deltaLatitudeE6 = (int) (this.mPanTotalLatitudeE6 * delta); + final int deltaLongitudeE6 = (int) (this.mPanTotalLongitudeE6 * delta); + + newMapCenterLatE6 = mapCenter.getLatitudeE6() - deltaLatitudeE6; + newMapCenterLonE6 = mapCenter.getLongitudeE6() - deltaLongitudeE6; + mapview.setMapCenter(new GeoPoint(newMapCenterLatE6, newMapCenterLonE6)); + + Thread.sleep(stepDuration); + } + mapview.setMapCenter(new GeoPoint(super.mTargetLatitudeE6, super.mTargetLongitudeE6)); + } catch (final Exception e) { + this.interrupt(); + } + } + } + + protected class QuarterCosinusalDeceleratingAnimationRunner extends + CosinusalBasedAnimationRunner implements MathConstants { + // =========================================================== + // Constructors + // =========================================================== + + protected QuarterCosinusalDeceleratingAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6) { + this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, + ANIMATION_DURATION_DEFAULT); + } + + protected QuarterCosinusalDeceleratingAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6, final int aSmoothness, final int aDuration) { + super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration, 0, PI_2, 0); + } + } + + protected class HalfCosinusalDeceleratingAnimationRunner extends CosinusalBasedAnimationRunner + implements MathConstants { + // =========================================================== + // Constructors + // =========================================================== + + protected HalfCosinusalDeceleratingAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6) { + this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, + ANIMATION_DURATION_DEFAULT); + } + + protected HalfCosinusalDeceleratingAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6, final int aSmoothness, final int aDuration) { + super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration, 0, PI, 1); + } + } + + protected class MiddlePeakSpeedAnimationRunner extends CosinusalBasedAnimationRunner implements + MathConstants { + // =========================================================== + // Constructors + // =========================================================== + + protected MiddlePeakSpeedAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6) { + this(aTargetLatitudeE6, aTargetLongitudeE6, ANIMATION_SMOOTHNESS_DEFAULT, + ANIMATION_DURATION_DEFAULT); + } + + protected MiddlePeakSpeedAnimationRunner(final int aTargetLatitudeE6, + final int aTargetLongitudeE6, final int aSmoothness, final int aDuration) { + super(aTargetLatitudeE6, aTargetLongitudeE6, aSmoothness, aDuration, -PI_2, PI, 0); + } + } +} diff --git a/src/main/java/org/osmdroid/views/MapView.java b/src/main/java/org/osmdroid/views/MapView.java new file mode 100644 index 000000000..090beb022 --- /dev/null +++ b/src/main/java/org/osmdroid/views/MapView.java @@ -0,0 +1,1721 @@ +// Created by plusminus on 17:45:56 - 25.09.2008 +package org.osmdroid.views; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.metalev.multitouch.controller.MultiTouchController; +import org.metalev.multitouch.controller.MultiTouchController.MultiTouchObjectCanvas; +import org.metalev.multitouch.controller.MultiTouchController.PointInfo; +import org.metalev.multitouch.controller.MultiTouchController.PositionAndScale; +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.api.IMap; +import org.osmdroid.api.IMapController; +import org.osmdroid.api.IMapView; +import org.osmdroid.api.IProjection; +import org.osmdroid.events.MapListener; +import org.osmdroid.events.ScrollEvent; +import org.osmdroid.events.ZoomEvent; +import org.osmdroid.tileprovider.MapTileProviderArray; +import org.osmdroid.tileprovider.MapTileProviderBase; +import org.osmdroid.tileprovider.MapTileProviderBasic; +import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase; +import org.osmdroid.tileprovider.tilesource.IStyledTileSource; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.tileprovider.util.SimpleInvalidationHandler; +import org.osmdroid.util.BoundingBoxE6; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.util.GeometryMath; +import org.osmdroid.util.constants.GeoConstants; +import org.osmdroid.views.overlay.Overlay; +import org.osmdroid.views.overlay.OverlayManager; +import org.osmdroid.views.overlay.TilesOverlay; +import org.osmdroid.views.safecanvas.ISafeCanvas; +import org.osmdroid.views.util.constants.MapViewConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.GestureDetector.OnGestureListener; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Scroller; +import android.widget.ZoomButtonsController; +import android.widget.ZoomButtonsController.OnZoomListener; + +import microsoft.mappoint.TileSystem; + +public class MapView extends ViewGroup implements IMapView, MapViewConstants, + MultiTouchObjectCanvas { + + // =========================================================== + // Constants + // =========================================================== + + private static final Logger logger = LoggerFactory.getLogger(MapView.class); + + private static final double ZOOM_SENSITIVITY = 1.0; + private static final double ZOOM_LOG_BASE_INV = 1.0 / Math.log(2.0 / ZOOM_SENSITIVITY); + private static Method sMotionEventTransformMethod; + + // =========================================================== + // Fields + // =========================================================== + + /** Current zoom level for map tiles. */ + private int mZoomLevel = 0; + + private final OverlayManager mOverlayManager; + + private Projection mProjection; + + private final TilesOverlay mMapOverlay; + + private final GestureDetector mGestureDetector; + + /** Handles map scrolling */ + private final Scroller mScroller; + protected boolean mIsFlinging; + + protected final AtomicInteger mTargetZoomLevel = new AtomicInteger(); + protected final AtomicBoolean mIsAnimating = new AtomicBoolean(false); + + protected Integer mMinimumZoomLevel; + protected Integer mMaximumZoomLevel; + + private final MapController mController; + + private final ZoomButtonsController mZoomController; + private boolean mEnableZoomController = false; + + private final ResourceProxy mResourceProxy; + + private MultiTouchController mMultiTouchController; + protected float mMultiTouchScale = 1.0f; + protected PointF mMultiTouchScalePoint = new PointF(); + + protected MapListener mListener; + + // For rotation + private float mapOrientation = 0; + private final Matrix mRotateMatrix = new Matrix(); + private final float[] mRotatePoints = new float[2]; + private final Rect mInvalidateRect = new Rect(); + + protected BoundingBoxE6 mScrollableAreaBoundingBox; + protected Rect mScrollableAreaLimit; + + // for speed (avoiding allocations) + private final MapTileProviderBase mTileProvider; + + private final Handler mTileRequestCompleteHandler; + + /* a point that will be reused to design added views */ + private final Point mPoint = new Point(); + + // =========================================================== + // Constructors + // =========================================================== + + protected MapView(final Context context, final int tileSizePixels, + final ResourceProxy resourceProxy, MapTileProviderBase tileProvider, + final Handler tileRequestCompleteHandler, final AttributeSet attrs) { + super(context, attrs); + mResourceProxy = resourceProxy; + this.mController = new MapController(this); + this.mScroller = new Scroller(context); + TileSystem.setTileSize(tileSizePixels); + + if (tileProvider == null) { + final ITileSource tileSource = getTileSourceFromAttributes(attrs); + tileProvider = isInEditMode() + ? new MapTileProviderArray(tileSource, null, new MapTileModuleProviderBase[0]) + : new MapTileProviderBasic(context, tileSource); + } + + mTileRequestCompleteHandler = tileRequestCompleteHandler == null + ? new SimpleInvalidationHandler(this) + : tileRequestCompleteHandler; + mTileProvider = tileProvider; + mTileProvider.setTileRequestCompleteHandler(mTileRequestCompleteHandler); + + this.mMapOverlay = new TilesOverlay(mTileProvider, mResourceProxy); + mOverlayManager = new OverlayManager(mMapOverlay); + + if (isInEditMode()) { + mZoomController = null; + } else { + mZoomController = new ZoomButtonsController(this); + mZoomController.setOnZoomListener(new MapViewZoomListener()); + } + + mGestureDetector = new GestureDetector(context, new MapViewGestureDetectorListener()); + mGestureDetector.setOnDoubleTapListener(new MapViewDoubleClickListener()); + } + + /** + * Constructor used by XML layout resource (uses default tile source). + */ + public MapView(final Context context, final AttributeSet attrs) { + this(context, 256, new DefaultResourceProxyImpl(context), null, null, attrs); + } + + /** + * Standard Constructor. + */ + public MapView(final Context context, final int tileSizePixels) { + this(context, tileSizePixels, new DefaultResourceProxyImpl(context)); + } + + public MapView(final Context context, final int tileSizePixels, + final ResourceProxy resourceProxy) { + this(context, tileSizePixels, resourceProxy, null); + } + + public MapView(final Context context, final int tileSizePixels, + final ResourceProxy resourceProxy, final MapTileProviderBase aTileProvider) { + this(context, tileSizePixels, resourceProxy, aTileProvider, null); + } + + public MapView(final Context context, final int tileSizePixels, + final ResourceProxy resourceProxy, final MapTileProviderBase aTileProvider, + final Handler tileRequestCompleteHandler) { + this(context, tileSizePixels, resourceProxy, aTileProvider, tileRequestCompleteHandler, + null); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + @Override + public IMapController getController() { + return this.mController; + } + + /** + * You can add/remove/reorder your Overlays using the List of {@link Overlay}. The first (index + * 0) Overlay gets drawn first, the one with the highest as the last one. + */ + public List getOverlays() { + return this.getOverlayManager(); + } + + public OverlayManager getOverlayManager() { + return mOverlayManager; + } + + public MapTileProviderBase getTileProvider() { + return mTileProvider; + } + + public Scroller getScroller() { + return mScroller; + } + + public Handler getTileRequestCompleteHandler() { + return mTileRequestCompleteHandler; + } + + @Override + public int getLatitudeSpan() { + return this.getBoundingBox().getLatitudeSpanE6(); + } + + @Override + public int getLongitudeSpan() { + return this.getBoundingBox().getLongitudeSpanE6(); + } + + public BoundingBoxE6 getBoundingBox() { + return getBoundingBox(getWidth(), getHeight()); + } + + public BoundingBoxE6 getBoundingBox(final int pViewWidth, final int pViewHeight) { + + final int world_2 = TileSystem.MapSize(mZoomLevel) / 2; + final Rect screenRect = getScreenRect(null); + screenRect.offset(world_2, world_2); + + final IGeoPoint neGeoPoint = TileSystem.PixelXYToLatLong(screenRect.right, screenRect.top, + mZoomLevel, null); + final IGeoPoint swGeoPoint = TileSystem.PixelXYToLatLong(screenRect.left, + screenRect.bottom, mZoomLevel, null); + + return new BoundingBoxE6(neGeoPoint.getLatitudeE6(), neGeoPoint.getLongitudeE6(), + swGeoPoint.getLatitudeE6(), swGeoPoint.getLongitudeE6()); + } + + /** + * Gets the current bounds of the screen in screen coordinates. + */ + public Rect getScreenRect(final Rect reuse) { + final Rect out = getIntrinsicScreenRect(reuse); + if (this.getMapOrientation() != 0 && this.getMapOrientation() != 180) { + // Since the canvas is shifted by getWidth/2, we can just return our natural scrollX/Y + // value since that is the same as the shifted center. + int centerX = this.getScrollX(); + int centerY = this.getScrollY(); + GeometryMath.getBoundingBoxForRotatatedRectangle(out, centerX, centerY, + this.getMapOrientation(), out); + } + return out; + } + + public Rect getIntrinsicScreenRect(final Rect reuse) { + final Rect out = reuse == null ? new Rect() : reuse; + out.set(getScrollX() - getWidth() / 2, getScrollY() - getHeight() / 2, getScrollX() + + getWidth() / 2, getScrollY() + getHeight() / 2); + return out; + } + + /** + * Get a projection for converting between screen-pixel coordinates and latitude/longitude + * coordinates. You should not hold on to this object for more than one draw, since the + * projection of the map could change. + * + * @return The Projection of the map in its current state. You should not hold on to this object + * for more than one draw, since the projection of the map could change. + */ + @Override + public Projection getProjection() { + if (mProjection == null) { + mProjection = new Projection(); + } + return mProjection; + } + + void setMapCenter(final IGeoPoint aCenter) { + getController().animateTo(aCenter); + } + + /** + * @deprecated use {@link #setMapCenter(IGeoPoint)} + */ + void setMapCenter(final int aLatitudeE6, final int aLongitudeE6) { + setMapCenter(new GeoPoint(aLatitudeE6, aLongitudeE6)); + } + + public void setTileSource(final ITileSource aTileSource) { + mTileProvider.setTileSource(aTileSource); + TileSystem.setTileSize(aTileSource.getTileSizePixels()); + this.checkZoomButtons(); + this.setZoomLevel(mZoomLevel); // revalidate zoom level + postInvalidate(); + } + + /** + * @param aZoomLevel + * the zoom level bound by the tile source + */ + int setZoomLevel(final int aZoomLevel) { + final int minZoomLevel = getMinZoomLevel(); + final int maxZoomLevel = getMaxZoomLevel(); + + final int newZoomLevel = Math.max(minZoomLevel, Math.min(maxZoomLevel, aZoomLevel)); + final int curZoomLevel = this.mZoomLevel; + + if (newZoomLevel != curZoomLevel) { + mScroller.forceFinished(true); + mIsFlinging = false; + } + + this.mZoomLevel = newZoomLevel; + this.checkZoomButtons(); + + if (newZoomLevel > curZoomLevel) { + // We are going from a lower-resolution plane to a higher-resolution plane, so we have + // to do it the hard way. + final int worldSize_current_2 = TileSystem.MapSize(curZoomLevel) / 2; + final int worldSize_new_2 = TileSystem.MapSize(newZoomLevel) / 2; + final IGeoPoint centerGeoPoint = TileSystem.PixelXYToLatLong(getScrollX() + + worldSize_current_2, getScrollY() + worldSize_current_2, curZoomLevel, null); + final Point centerPoint = TileSystem.LatLongToPixelXY( + centerGeoPoint.getLatitudeE6() / 1E6, centerGeoPoint.getLongitudeE6() / 1E6, + newZoomLevel, null); + scrollTo(centerPoint.x - worldSize_new_2, centerPoint.y - worldSize_new_2); + } else if (newZoomLevel < curZoomLevel) { + // We are going from a higher-resolution plane to a lower-resolution plane, so we can do + // it the easy way. + scrollTo(getScrollX() >> curZoomLevel - newZoomLevel, + getScrollY() >> curZoomLevel - newZoomLevel); + } + + // snap for all snappables + final Point snapPoint = new Point(); + mProjection = new Projection(); + if (this.getOverlayManager().onSnapToItem(getScrollX(), getScrollY(), snapPoint, this)) { + scrollTo(snapPoint.x, snapPoint.y); + } + + mTileProvider.rescaleCache(newZoomLevel, curZoomLevel, getScreenRect(null)); + + // do callback on listener + if (newZoomLevel != curZoomLevel && mListener != null) { + final ZoomEvent event = new ZoomEvent(this, newZoomLevel); + mListener.onZoom(event); + } + // Allows any views fixed to a Location in the MapView to adjust + this.requestLayout(); + return this.mZoomLevel; + } + + /** + * Zoom the map to enclose the specified bounding box, as closely as possible. + * Must be called after display layout is complete, or screen dimensions are not known, and + * will always zoom to center of zoom level 0. + * Suggestion: Check getScreenRect(null).getHeight() > 0 + */ + public void zoomToBoundingBox(final BoundingBoxE6 boundingBox) { + final BoundingBoxE6 currentBox = getBoundingBox(); + + // Calculated required zoom based on latitude span + final double maxZoomLatitudeSpan = mZoomLevel == getMaxZoomLevel() ? + currentBox.getLatitudeSpanE6() : + currentBox.getLatitudeSpanE6() / Math.pow(2, getMaxZoomLevel() - mZoomLevel); + + final double requiredLatitudeZoom = + getMaxZoomLevel() - + Math.ceil(Math.log(boundingBox.getLatitudeSpanE6() / maxZoomLatitudeSpan) / Math.log(2)); + + + // Calculated required zoom based on longitude span + final double maxZoomLongitudeSpan = mZoomLevel == getMaxZoomLevel() ? + currentBox.getLongitudeSpanE6() : + currentBox.getLongitudeSpanE6() / Math.pow(2, getMaxZoomLevel() - mZoomLevel); + + final double requiredLongitudeZoom = + getMaxZoomLevel() - + Math.ceil(Math.log(boundingBox.getLongitudeSpanE6() / maxZoomLongitudeSpan) / Math.log(2)); + + + // Zoom to boundingBox center, at calculated maximum allowed zoom level + getController().setZoom((int)( + requiredLatitudeZoom < requiredLongitudeZoom ? + requiredLatitudeZoom : requiredLongitudeZoom)); + + getController().setCenter( + new GeoPoint(boundingBox.getCenter().getLatitudeE6(), boundingBox.getCenter() + .getLongitudeE6())); + } + + /** + * Get the current ZoomLevel for the map tiles. + * + * @return the current ZoomLevel between 0 (equator) and 18/19(closest), depending on the tile + * source chosen. + */ + @Override + public int getZoomLevel() { + return getZoomLevel(true); + } + + /** + * Get the current ZoomLevel for the map tiles. + * + * @param aPending + * if true and we're animating then return the zoom level that we're animating + * towards, otherwise return the current zoom level + * @return the zoom level + */ + public int getZoomLevel(final boolean aPending) { + if (aPending && isAnimating()) { + return mTargetZoomLevel.get(); + } else { + return mZoomLevel; + } + } + + /** + * Get the minimum allowed zoom level for the maps. + */ + public int getMinZoomLevel() { + return mMinimumZoomLevel == null ? mMapOverlay.getMinimumZoomLevel() : mMinimumZoomLevel; + } + + /** + * Get the maximum allowed zoom level for the maps. + */ + @Override + public int getMaxZoomLevel() { + return mMaximumZoomLevel == null ? mMapOverlay.getMaximumZoomLevel() : mMaximumZoomLevel; + } + + /** + * Set the minimum allowed zoom level, or pass null to use the minimum zoom level from the tile + * provider. + */ + public void setMinZoomLevel(Integer zoomLevel) { + mMinimumZoomLevel = zoomLevel; + } + + /** + * Set the maximum allowed zoom level, or pass null to use the maximum zoom level from the tile + * provider. + */ + public void setMaxZoomLevel(Integer zoomLevel) { + mMaximumZoomLevel = zoomLevel; + } + + public boolean canZoomIn() { + final int maxZoomLevel = getMaxZoomLevel(); + if ((isAnimating() ? mTargetZoomLevel.get() : mZoomLevel) >= maxZoomLevel) { + return false; + } + return true; + } + + public boolean canZoomOut() { + final int minZoomLevel = getMinZoomLevel(); + if ((isAnimating() ? mTargetZoomLevel.get() : mZoomLevel) <= minZoomLevel) { + return false; + } + return true; + } + + /** + * Zoom in by one zoom level. + */ + boolean zoomIn() { + return getController().zoomIn(); + } + + boolean zoomInFixing(final IGeoPoint point) { + Point coords = getProjection().toMapPixels(point, null); + return getController().zoomInFixing(coords.x, coords.y); + } + + boolean zoomInFixing(final int xPixel, final int yPixel) { + return getController().zoomInFixing(xPixel, yPixel); + } + + /** + * Zoom out by one zoom level. + */ + boolean zoomOut() { + return getController().zoomOut(); + } + + boolean zoomOutFixing(final IGeoPoint point) { + Point coords = getProjection().toMapPixels(point, null); + return zoomOutFixing(coords.x, coords.y); + } + + boolean zoomOutFixing(final int xPixel, final int yPixel) { + return getController().zoomOutFixing(xPixel, yPixel); + } + + /** + * Returns the current center-point position of the map, as a GeoPoint (latitude and longitude). + * + * @return A GeoPoint of the map's center-point. + */ + @Override + public IGeoPoint getMapCenter() { + final int world_2 = TileSystem.MapSize(mZoomLevel) / 2; + final Rect screenRect = getScreenRect(null); + screenRect.offset(world_2, world_2); + return TileSystem.PixelXYToLatLong(screenRect.centerX(), screenRect.centerY(), mZoomLevel, + null); + } + + public ResourceProxy getResourceProxy() { + return mResourceProxy; + } + + public void setMapOrientation(float degrees) { + this.mapOrientation = degrees % 360.0f; + this.invalidate(); + } + + public float getMapOrientation() { + return mapOrientation; + } + + /** + * Whether to use the network connection if it's available. + */ + public boolean useDataConnection() { + return mMapOverlay.useDataConnection(); + } + + /** + * Set whether to use the network connection if it's available. + * + * @param aMode + * if true use the network connection if it's available. if false don't use the + * network connection even if it's available. + */ + public void setUseDataConnection(final boolean aMode) { + mMapOverlay.setUseDataConnection(aMode); + } + + /** + * Set the map to limit it's scrollable view to the specified BoundingBoxE6. Note this does not + * limit zooming so it will be possible for the user to zoom to an area that is larger than the + * limited area. + * + * @param boundingBox + * A lat/long bounding box to limit scrolling to, or null to remove any scrolling + * limitations + */ + public void setScrollableAreaLimit(BoundingBoxE6 boundingBox) { + final int worldSize_2 = TileSystem.MapSize(MapViewConstants.MAXIMUM_ZOOMLEVEL) / 2; + + mScrollableAreaBoundingBox = boundingBox; + + // Clear scrollable area limit if null passed. + if (boundingBox == null) { + mScrollableAreaLimit = null; + return; + } + + // Get NW/upper-left + final Point upperLeft = TileSystem.LatLongToPixelXY(boundingBox.getLatNorthE6() / 1E6, + boundingBox.getLonWestE6() / 1E6, MapViewConstants.MAXIMUM_ZOOMLEVEL, null); + upperLeft.offset(-worldSize_2, -worldSize_2); + + // Get SE/lower-right + final Point lowerRight = TileSystem.LatLongToPixelXY(boundingBox.getLatSouthE6() / 1E6, + boundingBox.getLonEastE6() / 1E6, MapViewConstants.MAXIMUM_ZOOMLEVEL, null); + lowerRight.offset(-worldSize_2, -worldSize_2); + mScrollableAreaLimit = new Rect(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y); + } + + public BoundingBoxE6 getScrollableAreaLimit() { + return mScrollableAreaBoundingBox; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + public void invalidateMapCoordinates(Rect dirty) { + invalidateMapCoordinates(dirty.left, dirty.top, dirty.right, dirty.bottom); + } + + public void invalidateMapCoordinates(int left, int top, int right, int bottom) { + mInvalidateRect.set(left, top, right, bottom); + final int width_2 = this.getWidth() / 2; + final int height_2 = this.getHeight() / 2; + + // Since the canvas is shifted by getWidth/2, we can just return our natural scrollX/Y value + // since that is the same as the shifted center. + int centerX = this.getScrollX(); + int centerY = this.getScrollY(); + + if (this.getMapOrientation() != 0) + GeometryMath.getBoundingBoxForRotatatedRectangle(mInvalidateRect, centerX, centerY, + this.getMapOrientation() + 180, mInvalidateRect); + mInvalidateRect.offset(width_2, height_2); + + super.invalidate(mInvalidateRect); + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, a height of + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} at the {@link GeoPoint} (0, 0) align + * with {@link MapView.LayoutParams#BOTTOM_CENTER}. + */ + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new MapView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, null, MapView.LayoutParams.BOTTOM_CENTER, 0, 0); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) { + return new MapView.LayoutParams(getContext(), attrs); + } + + // Override to allow type-checking of LayoutParams. + @Override + protected boolean checkLayoutParams(final ViewGroup.LayoutParams p) { + return p instanceof MapView.LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) { + return new MapView.LayoutParams(p); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + final int count = getChildCount(); + + int maxHeight = 0; + int maxWidth = 0; + + // Find out how big everyone wants to be + measureChildren(widthMeasureSpec, heightMeasureSpec); + + // Find rightmost and bottom-most child + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + + final MapView.LayoutParams lp = (MapView.LayoutParams) child.getLayoutParams(); + final int childHeight = child.getMeasuredHeight(); + final int childWidth = child.getMeasuredWidth(); + getProjection().toMapPixels(lp.geoPoint, mPoint); + final int x = mPoint.x + getWidth() / 2; + final int y = mPoint.y + getHeight() / 2; + int childRight = x; + int childBottom = y; + switch (lp.alignment) { + case MapView.LayoutParams.TOP_LEFT: + childRight = x + childWidth; + childBottom = y; + break; + case MapView.LayoutParams.TOP_CENTER: + childRight = x + childWidth / 2; + childBottom = y; + break; + case MapView.LayoutParams.TOP_RIGHT: + childRight = x; + childBottom = y; + break; + case MapView.LayoutParams.CENTER_LEFT: + childRight = x + childWidth; + childBottom = y + childHeight / 2; + break; + case MapView.LayoutParams.CENTER: + childRight = x + childWidth / 2; + childBottom = y + childHeight / 2; + break; + case MapView.LayoutParams.CENTER_RIGHT: + childRight = x; + childBottom = y + childHeight / 2; + break; + case MapView.LayoutParams.BOTTOM_LEFT: + childRight = x + childWidth; + childBottom = y + childHeight; + break; + case MapView.LayoutParams.BOTTOM_CENTER: + childRight = x + childWidth / 2; + childBottom = y + childHeight; + break; + case MapView.LayoutParams.BOTTOM_RIGHT: + childRight = x; + childBottom = y + childHeight; + break; + } + childRight += lp.offsetX; + childBottom += lp.offsetY; + + maxWidth = Math.max(maxWidth, childRight); + maxHeight = Math.max(maxHeight, childBottom); + } + } + + // Account for padding too + maxWidth += getPaddingLeft() + getPaddingRight(); + maxHeight += getPaddingTop() + getPaddingBottom(); + + // Check against minimum height and width + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), + resolveSize(maxHeight, heightMeasureSpec)); + } + + @Override + protected void onLayout(final boolean changed, final int l, final int t, final int r, + final int b) { + final int count = getChildCount(); + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + + final MapView.LayoutParams lp = (MapView.LayoutParams) child.getLayoutParams(); + final int childHeight = child.getMeasuredHeight(); + final int childWidth = child.getMeasuredWidth(); + getProjection().toMapPixels(lp.geoPoint, mPoint); + final int x = mPoint.x + getWidth() / 2; + final int y = mPoint.y + getHeight() / 2; + int childLeft = x; + int childTop = y; + switch (lp.alignment) { + case MapView.LayoutParams.TOP_LEFT: + childLeft = getPaddingLeft() + x; + childTop = getPaddingTop() + y; + break; + case MapView.LayoutParams.TOP_CENTER: + childLeft = getPaddingLeft() + x - childWidth / 2; + childTop = getPaddingTop() + y; + break; + case MapView.LayoutParams.TOP_RIGHT: + childLeft = getPaddingLeft() + x - childWidth; + childTop = getPaddingTop() + y; + break; + case MapView.LayoutParams.CENTER_LEFT: + childLeft = getPaddingLeft() + x; + childTop = getPaddingTop() + y - childHeight / 2; + break; + case MapView.LayoutParams.CENTER: + childLeft = getPaddingLeft() + x - childWidth / 2; + childTop = getPaddingTop() + y - childHeight / 2; + break; + case MapView.LayoutParams.CENTER_RIGHT: + childLeft = getPaddingLeft() + x - childWidth; + childTop = getPaddingTop() + y - childHeight / 2; + break; + case MapView.LayoutParams.BOTTOM_LEFT: + childLeft = getPaddingLeft() + x; + childTop = getPaddingTop() + y - childHeight; + break; + case MapView.LayoutParams.BOTTOM_CENTER: + childLeft = getPaddingLeft() + x - childWidth / 2; + childTop = getPaddingTop() + y - childHeight; + break; + case MapView.LayoutParams.BOTTOM_RIGHT: + childLeft = getPaddingLeft() + x - childWidth; + childTop = getPaddingTop() + y - childHeight; + break; + } + childLeft += lp.offsetX; + childTop += lp.offsetY; + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + } + } + } + + public void onDetach() { + this.getOverlayManager().onDetach(this); + } + + @Override + public boolean onKeyDown(final int keyCode, final KeyEvent event) { + final boolean result = this.getOverlayManager().onKeyDown(keyCode, event, this); + + return result || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(final int keyCode, final KeyEvent event) { + final boolean result = this.getOverlayManager().onKeyUp(keyCode, event, this); + + return result || super.onKeyUp(keyCode, event); + } + + @Override + public boolean onTrackballEvent(final MotionEvent event) { + + if (this.getOverlayManager().onTrackballEvent(event, this)) { + return true; + } + + scrollBy((int) (event.getX() * 25), (int) (event.getY() * 25)); + + return super.onTrackballEvent(event); + } + + @Override + public boolean dispatchTouchEvent(final MotionEvent event) { + + if (DEBUGMODE) { + logger.debug("dispatchTouchEvent(" + event + ")"); + } + + if (mZoomController.isVisible() && mZoomController.onTouch(this, event)) { + return true; + } + + // Get rotated event for some touch listeners. + MotionEvent rotatedEvent = rotateTouchEvent(event); + + try { + if (super.dispatchTouchEvent(event)) { + if (DEBUGMODE) { + logger.debug("super handled onTouchEvent"); + } + return true; + } + + if (this.getOverlayManager().onTouchEvent(rotatedEvent, this)) { + return true; + } + + if (mMultiTouchController != null && mMultiTouchController.onTouchEvent(event)) { + if (DEBUGMODE) { + logger.debug("mMultiTouchController handled onTouchEvent"); + } + return true; + } + + if (mGestureDetector.onTouchEvent(rotatedEvent)) { + if (DEBUGMODE) { + logger.debug("mGestureDetector handled onTouchEvent"); + } + return true; + } + } finally { + if (rotatedEvent != event) + rotatedEvent.recycle(); + } + + if (DEBUGMODE) { + logger.debug("no-one handled onTouchEvent"); + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return false; + } + + private MotionEvent rotateTouchEvent(MotionEvent ev) { + if (this.getMapOrientation() == 0) + return ev; + + mRotateMatrix.setRotate(-getMapOrientation(), this.getWidth() / 2, this.getHeight() / 2); + + MotionEvent rotatedEvent = MotionEvent.obtain(ev); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + mRotatePoints[0] = ev.getX(); + mRotatePoints[1] = ev.getY(); + mRotateMatrix.mapPoints(mRotatePoints); + rotatedEvent.setLocation(mRotatePoints[0], mRotatePoints[1]); + } else { + // This method is preferred since it will rotate historical touch events too + try { + if (sMotionEventTransformMethod == null) { + sMotionEventTransformMethod = MotionEvent.class.getDeclaredMethod("transform", + new Class[] { Matrix.class }); + } + sMotionEventTransformMethod.invoke(rotatedEvent, mRotateMatrix); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + return rotatedEvent; + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + if (mScroller.isFinished()) { + // One last scrollTo to get to the final destination + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + // This will facilitate snapping-to any Snappable points. + setZoomLevel(mZoomLevel); + mIsFlinging = false; + } else { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + } + postInvalidate(); // Keep on drawing until the animation has + // finished. + } + } + + @Override + public void scrollTo(int x, int y) { + final int worldSize_2 = TileSystem.MapSize(this.getZoomLevel(false)) / 2; + while (x < -worldSize_2) { + x += worldSize_2 * 2; + } + while (x > worldSize_2) { + x -= worldSize_2 * 2; + } + while (y < -worldSize_2) { + y += worldSize_2 * 2; + } + while (y > worldSize_2) { + y -= worldSize_2 * 2; + } + + if (mScrollableAreaLimit != null) { + final int zoomDiff = MapViewConstants.MAXIMUM_ZOOMLEVEL - getZoomLevel(false); + final int minX = (mScrollableAreaLimit.left >> zoomDiff); + final int minY = (mScrollableAreaLimit.top >> zoomDiff); + final int maxX = (mScrollableAreaLimit.right >> zoomDiff); + final int maxY = (mScrollableAreaLimit.bottom >> zoomDiff); + + final int scrollableWidth = maxX - minX; + final int scrollableHeight = maxY - minY; + final int width = this.getWidth(); + final int height = this.getHeight(); + + // Adjust if we are outside the scrollable area + if (scrollableWidth <= width) { + if (x - (width / 2) > minX) + x = minX + (width / 2); + else if (x + (width / 2) < maxX) + x = maxX - (width / 2); + } else if (x - (width / 2) < minX) + x = minX + (width / 2); + else if (x + (width / 2) > maxX) + x = maxX - (width / 2); + + if (scrollableHeight <= height) { + if (y - (height / 2) > minY) + y = minY + (height / 2); + else if (y + (height / 2) < maxY) + y = maxY - (height / 2); + } else if (y - (height / 2) < minY) + y = minY + (height / 2); + else if (y + (height / 2) > maxY) + y = maxY - (height / 2); + } + super.scrollTo(x, y); + + // do callback on listener + if (mListener != null) { + final ScrollEvent event = new ScrollEvent(this, x, y); + mListener.onScroll(event); + } + } + + @Override + public void setBackgroundColor(final int pColor) { + mMapOverlay.setLoadingBackgroundColor(pColor); + invalidate(); + } + + @Override + protected void dispatchDraw(final Canvas c) { + final long startMs = System.currentTimeMillis(); + + mProjection = new Projection(); + + // Save the current canvas matrix + c.save(); + + c.translate(getWidth() / 2, getHeight() / 2); + c.scale(mMultiTouchScale, mMultiTouchScale, mMultiTouchScalePoint.x, + mMultiTouchScalePoint.y); + + /* rotate Canvas */ + c.rotate(mapOrientation, mProjection.getScreenRect().exactCenterX(), mProjection + .getScreenRect().exactCenterY()); + + /* Draw background */ + // c.drawColor(mBackgroundColor); + + /* Draw all Overlays. */ + this.getOverlayManager().onDraw(c, this); + + // Restore the canvas matrix + c.restore(); + + super.dispatchDraw(c); + + if (DEBUGMODE) { + final long endMs = System.currentTimeMillis(); + logger.debug("Rendering overall: " + (endMs - startMs) + "ms"); + } + } + + /** + * Returns true if the safe drawing canvas is being used. + * + * @see {@link ISafeCanvas} + */ + public boolean isUsingSafeCanvas() { + return this.getOverlayManager().isUsingSafeCanvas(); + } + + /** + * Sets whether the safe drawing canvas is being used. + * + * @see {@link ISafeCanvas} + */ + public void setUseSafeCanvas(boolean useSafeCanvas) { + this.getOverlayManager().setUseSafeCanvas(useSafeCanvas); + } + + @Override + protected void onDetachedFromWindow() { + this.mZoomController.setVisible(false); + this.onDetach(); + super.onDetachedFromWindow(); + } + + // =========================================================== + // Animation + // =========================================================== + + /** + * Determines if maps are animating a zoom operation. Useful for overlays to avoid recalculating + * during an animation sequence. + * + * @return boolean indicating whether view is animating. + */ + public boolean isAnimating() { + return mIsAnimating.get(); + } + + // =========================================================== + // Implementation of MultiTouchObjectCanvas + // =========================================================== + + @Override + public Object getDraggableObjectAtPoint(final PointInfo pt) { + if (this.isAnimating()) { + // Zoom animations use the mMultiTouchScale variables to perform their animations so we + // don't want to step on that. + return null; + } else { + mMultiTouchScalePoint.x = pt.getX() + getScrollX() - (this.getWidth() / 2); + mMultiTouchScalePoint.y = pt.getY() + getScrollY() - (this.getHeight() / 2); + return this; + } + } + + @Override + public void getPositionAndScale(final Object obj, final PositionAndScale objPosAndScaleOut) { + objPosAndScaleOut.set(0, 0, true, mMultiTouchScale, false, 0, 0, false, 0); + } + + @Override + public void selectObject(final Object obj, final PointInfo pt) { + // if obj is null it means we released the pointers + // if scale is not 1 it means we pinched + if (obj == null && mMultiTouchScale != 1.0f) { + final float scaleDiffFloat = (float) (Math.log(mMultiTouchScale) * ZOOM_LOG_BASE_INV); + final int scaleDiffInt = Math.round(scaleDiffFloat); + // If we are changing zoom levels, + // adjust the center point in respect to the scaling point + if (scaleDiffInt != 0) { + Matrix m = new Matrix(); + m.setScale(1 / mMultiTouchScale, 1 / mMultiTouchScale, mMultiTouchScalePoint.x, + mMultiTouchScalePoint.y); + m.postRotate(-mapOrientation, mProjection.getScreenRect().centerX(), mProjection + .getScreenRect().centerY()); + float[] pts = new float[2]; + pts[0] = getScrollX(); + pts[1] = getScrollY(); + m.mapPoints(pts); + scrollTo((int) pts[0], (int) pts[1]); + } + + // Adjust the zoomLevel + setZoomLevel(mZoomLevel + scaleDiffInt); + } + + // reset scale + mMultiTouchScale = 1.0f; + } + + @Override + public boolean setPositionAndScale(final Object obj, final PositionAndScale aNewObjPosAndScale, + final PointInfo aTouchPoint) { + float multiTouchScale = aNewObjPosAndScale.getScale(); + // If we are at the first or last zoom level, prevent pinching/expanding + if (multiTouchScale > 1 && !canZoomIn()) { + multiTouchScale = 1; + } + if (multiTouchScale < 1 && !canZoomOut()) { + multiTouchScale = 1; + } + mMultiTouchScale = multiTouchScale; + invalidate(); // redraw + return true; + } + + /* + * Set the MapListener for this view + */ + public void setMapListener(final MapListener ml) { + mListener = ml; + } + + // =========================================================== + // Methods + // =========================================================== + + private void checkZoomButtons() { + this.mZoomController.setZoomInEnabled(canZoomIn()); + this.mZoomController.setZoomOutEnabled(canZoomOut()); + } + + public void setBuiltInZoomControls(final boolean on) { + this.mEnableZoomController = on; + this.checkZoomButtons(); + } + + public void setMultiTouchControls(final boolean on) { + mMultiTouchController = on ? new MultiTouchController(this, false) : null; + } + + private ITileSource getTileSourceFromAttributes(final AttributeSet aAttributeSet) { + + ITileSource tileSource = TileSourceFactory.DEFAULT_TILE_SOURCE; + + if (aAttributeSet != null) { + final String tileSourceAttr = aAttributeSet.getAttributeValue(null, "tilesource"); + if (tileSourceAttr != null) { + try { + final ITileSource r = TileSourceFactory.getTileSource(tileSourceAttr); + logger.info("Using tile source specified in layout attributes: " + r); + tileSource = r; + } catch (final IllegalArgumentException e) { + logger.warn("Invalid tile source specified in layout attributes: " + tileSource); + } + } + } + + if (aAttributeSet != null && tileSource instanceof IStyledTileSource) { + final String style = aAttributeSet.getAttributeValue(null, "style"); + if (style == null) { + logger.info("Using default style: 1"); + } else { + logger.info("Using style specified in layout attributes: " + style); + ((IStyledTileSource) tileSource).setStyle(style); + } + } + + logger.info("Using tile source: " + tileSource); + return tileSource; + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + /** + * A Projection serves to translate between the coordinate system of x/y on-screen pixel + * coordinates and that of latitude/longitude points on the surface of the earth. You obtain a + * Projection from MapView.getProjection(). You should not hold on to this object for more than + * one draw, since the projection of the map could change.
+ *
+ * Screen coordinates are in the coordinate system of the screen's Canvas. The origin is + * in the center of the plane. Screen coordinates are appropriate for using to draw to + * the screen.
+ *
+ * Map coordinates are in the coordinate system of the standard Mercator projection. The + * origin is in the upper-left corner of the plane. Map coordinates are appropriate for + * use in the TileSystem class.
+ *
+ * Intermediate coordinates are used to cache the computationally heavy part of the + * projection. They aren't suitable for use until translated into screen coordinates or + * map coordinates. + * + * @author Nicolas Gramlich + * @author Manuel Stahl + */ + public class Projection implements IProjection, GeoConstants { + + private final int viewWidth_2 = getWidth() / 2; + private final int viewHeight_2 = getHeight() / 2; + private final int worldSize_2 = TileSystem.MapSize(mZoomLevel) / 2; + private final int offsetX = -worldSize_2; + private final int offsetY = -worldSize_2; + + private final BoundingBoxE6 mBoundingBoxProjection; + private final int mZoomLevelProjection; + private final Rect mScreenRectProjection; + private final Rect mIntrinsicScreenRectProjection; + private final float mMapOrientation; + + private Projection() { + + /* + * Do some calculations and drag attributes to local variables to save some performance. + */ + mZoomLevelProjection = MapView.this.mZoomLevel; + mBoundingBoxProjection = MapView.this.getBoundingBox(); + mScreenRectProjection = MapView.this.getScreenRect(null); + mIntrinsicScreenRectProjection = MapView.this.getIntrinsicScreenRect(null); + mMapOrientation = MapView.this.getMapOrientation(); + } + + public int getZoomLevel() { + return mZoomLevelProjection; + } + + public BoundingBoxE6 getBoundingBox() { + return mBoundingBoxProjection; + } + + public Rect getScreenRect() { + return mScreenRectProjection; + } + + public Rect getIntrinsicScreenRect() { + return mIntrinsicScreenRectProjection; + } + + public float getMapOrientation() { + return mMapOrientation; + } + + /** + * @deprecated Use TileSystem.getTileSize() instead. + */ + @Deprecated + public int getTileSizePixels() { + return TileSystem.getTileSize(); + } + + /** + * @deprecated Use + * Point out = TileSystem.PixelXYToTileXY(screenRect.centerX(), screenRect.centerY(), null); + * instead. + */ + @Deprecated + public Point getCenterMapTileCoords() { + final Rect rect = getScreenRect(); + return TileSystem.PixelXYToTileXY(rect.centerX(), rect.centerY(), null); + } + + /** + * @deprecated Use + * final Point out = TileSystem.TileXYToPixelXY(centerMapTileCoords.x, centerMapTileCoords.y, null); + * instead. + */ + @Deprecated + public Point getUpperLeftCornerOfCenterMapTile() { + final Point centerMapTileCoords = getCenterMapTileCoords(); + return TileSystem.TileXYToPixelXY(centerMapTileCoords.x, centerMapTileCoords.y, null); + } + + /** + * Converts screen coordinates to the underlying GeoPoint. + * + * @param x + * @param y + * @return GeoPoint under x/y. + */ + public IGeoPoint fromPixels(final float x, final float y) { + final Rect screenRect = getIntrinsicScreenRect(); + return TileSystem.PixelXYToLatLong(screenRect.left + (int) x + worldSize_2, + screenRect.top + (int) y + worldSize_2, mZoomLevelProjection, null); + } + + public Point fromMapPixels(final int x, final int y, final Point reuse) { + final Point out = reuse != null ? reuse : new Point(); + out.set(x - viewWidth_2, y - viewHeight_2); + out.offset(getScrollX(), getScrollY()); + return out; + } + + /** + * Converts a GeoPoint to its screen coordinates. + * + * @param in + * the GeoPoint you want the screen coordinates of + * @param reuse + * just pass null if you do not have a Point to be 'recycled'. + * @return the Point containing the screen coordinates of the GeoPoint passed. + */ + public Point toMapPixels(final IGeoPoint in, final Point reuse) { + final Point out = reuse != null ? reuse : new Point(); + TileSystem.LatLongToPixelXY( + in.getLatitudeE6() / 1E6, + in.getLongitudeE6() / 1E6, + getZoomLevel(), out); + out.offset(offsetX, offsetY); + if (Math.abs(out.x - getScrollX()) > + Math.abs(out.x - TileSystem.MapSize(getZoomLevel()) - getScrollX())) { + out.x -= TileSystem.MapSize(getZoomLevel()); + } + if (Math.abs(out.x - getScrollX()) > + Math.abs(out.x + TileSystem.MapSize(getZoomLevel()) - getScrollX())) { + out.x += TileSystem.MapSize(getZoomLevel()); + } + if (Math.abs(out.y - getScrollY()) > + Math.abs(out.y - TileSystem.MapSize(getZoomLevel()) - getScrollY())) { + out.y -= TileSystem.MapSize(getZoomLevel()); + } + if (Math.abs(out.y - getScrollY()) > + Math.abs(out.y + TileSystem.MapSize(getZoomLevel()) - getScrollY())) { + out.y += TileSystem.MapSize(getZoomLevel()); + } + return out; + } + + /** + * Performs only the first computationally heavy part of the projection. Call + * toMapPixelsTranslated to get the final position. + * + * @param latituteE6 + * the latitute of the point + * @param longitudeE6 + * the longitude of the point + * @param reuse + * just pass null if you do not have a Point to be 'recycled'. + * @return intermediate value to be stored and passed to toMapPixelsTranslated. + */ + public Point toMapPixelsProjected(final int latituteE6, final int longitudeE6, + final Point reuse) { + final Point out = reuse != null ? reuse : new Point(); + + TileSystem + .LatLongToPixelXY(latituteE6 / 1E6, longitudeE6 / 1E6, MAXIMUM_ZOOMLEVEL, out); + return out; + } + + /** + * Performs the second computationally light part of the projection. Returns results in + * screen coordinates. + * + * @param in + * the Point calculated by the toMapPixelsProjected + * @param reuse + * just pass null if you do not have a Point to be 'recycled'. + * @return the Point containing the Screen coordinates of the initial GeoPoint passed + * to the toMapPixelsProjected. + */ + public Point toMapPixelsTranslated(final Point in, final Point reuse) { + final Point out = reuse != null ? reuse : new Point(); + + final int zoomDifference = MAXIMUM_ZOOMLEVEL - getZoomLevel(); + out.set((in.x >> zoomDifference) + offsetX, (in.y >> zoomDifference) + offsetY); + return out; + } + + /** + * Translates a rectangle from screen coordinates to intermediate coordinates. + * + * @param in + * the rectangle in screen coordinates + * @return a rectangle in intermediate coordindates. + */ + public Rect fromPixelsToProjected(final Rect in) { + final Rect result = new Rect(); + + final int zoomDifference = MAXIMUM_ZOOMLEVEL - getZoomLevel(); + + final int x0 = in.left - offsetX << zoomDifference; + final int x1 = in.right - offsetX << zoomDifference; + final int y0 = in.bottom - offsetY << zoomDifference; + final int y1 = in.top - offsetY << zoomDifference; + + result.set(Math.min(x0, x1), Math.min(y0, y1), Math.max(x0, x1), Math.max(y0, y1)); + return result; + } + + /** + * @deprecated Use TileSystem.TileXYToPixelXY + */ + @Deprecated + public Point toPixels(final Point tileCoords, final Point reuse) { + return toPixels(tileCoords.x, tileCoords.y, reuse); + } + + /** + * @deprecated Use TileSystem.TileXYToPixelXY + */ + @Deprecated + public Point toPixels(final int tileX, final int tileY, final Point reuse) { + return TileSystem.TileXYToPixelXY(tileX, tileY, reuse); + } + + // not presently used + public Rect toPixels(final BoundingBoxE6 pBoundingBoxE6) { + final Rect rect = new Rect(); + + final Point reuse = new Point(); + + toMapPixels( + new GeoPoint(pBoundingBoxE6.getLatNorthE6(), pBoundingBoxE6.getLonWestE6()), + reuse); + rect.left = reuse.x; + rect.top = reuse.y; + + toMapPixels( + new GeoPoint(pBoundingBoxE6.getLatSouthE6(), pBoundingBoxE6.getLonEastE6()), + reuse); + rect.right = reuse.x; + rect.bottom = reuse.y; + + return rect; + } + + @Override + public float metersToEquatorPixels(final float meters) { + return meters / (float) TileSystem.GroundResolution(0, mZoomLevelProjection); + } + + @Override + public IGeoPoint getNorthEast() { + return fromPixels(getWidth(), 0); + } + + @Override + public IGeoPoint getSouthWest() { + return fromPixels(0, getHeight()); + } + + @Override + public Point toPixels(final IGeoPoint in, final Point out) { + return toMapPixels(in, out); + } + + @Override + public IGeoPoint fromPixels(final int x, final int y) { + return fromPixels((float) x, (float) y); + } + } + + private class MapViewGestureDetectorListener implements OnGestureListener { + + @Override + public boolean onDown(final MotionEvent e) { + + // Stop scrolling if we are in the middle of a fling! + if (mIsFlinging) { + mScroller.abortAnimation(); + mIsFlinging = false; + } + + if (MapView.this.getOverlayManager().onDown(e, MapView.this)) { + return true; + } + + mZoomController.setVisible(mEnableZoomController); + return true; + } + + @Override + public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, + final float velocityY) { + if (MapView.this.getOverlayManager() + .onFling(e1, e2, velocityX, velocityY, MapView.this)) { + return true; + } + + final int worldSize = TileSystem.MapSize(MapView.this.getZoomLevel(false)); + mIsFlinging = true; + mScroller.fling(getScrollX(), getScrollY(), (int) -velocityX, (int) -velocityY, + -worldSize, worldSize, -worldSize, worldSize); + return true; + } + + @Override + public void onLongPress(final MotionEvent e) { + if (mMultiTouchController != null && mMultiTouchController.isPinching()) { + return; + } + MapView.this.getOverlayManager().onLongPress(e, MapView.this); + } + + @Override + public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX, + final float distanceY) { + if (MapView.this.getOverlayManager().onScroll(e1, e2, distanceX, distanceY, + MapView.this)) { + return true; + } + + scrollBy((int) distanceX, (int) distanceY); + return true; + } + + @Override + public void onShowPress(final MotionEvent e) { + MapView.this.getOverlayManager().onShowPress(e, MapView.this); + } + + @Override + public boolean onSingleTapUp(final MotionEvent e) { + if (MapView.this.getOverlayManager().onSingleTapUp(e, MapView.this)) { + return true; + } + + return false; + } + + } + + private class MapViewDoubleClickListener implements GestureDetector.OnDoubleTapListener { + @Override + public boolean onDoubleTap(final MotionEvent e) { + if (MapView.this.getOverlayManager().onDoubleTap(e, MapView.this)) { + return true; + } + + final IGeoPoint center = getProjection().fromPixels(e.getX(), e.getY()); + return zoomInFixing(center); + } + + @Override + public boolean onDoubleTapEvent(final MotionEvent e) { + if (MapView.this.getOverlayManager().onDoubleTapEvent(e, MapView.this)) { + return true; + } + + return false; + } + + @Override + public boolean onSingleTapConfirmed(final MotionEvent e) { + if (MapView.this.getOverlayManager().onSingleTapConfirmed(e, MapView.this)) { + return true; + } + + return false; + } + } + + private class MapViewZoomListener implements OnZoomListener { + @Override + public void onZoom(final boolean zoomIn) { + if (zoomIn) { + getController().zoomIn(); + } else { + getController().zoomOut(); + } + } + + @Override + public void onVisibilityChanged(final boolean visible) { + } + } + + // =========================================================== + // Public Classes + // =========================================================== + + /** + * Per-child layout information associated with OpenStreetMapView. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + + /** + * Special value for the alignment requested by a View. TOP_LEFT means that the location + * will at the top left the View. + */ + public static final int TOP_LEFT = 1; + /** + * Special value for the alignment requested by a View. TOP_RIGHT means that the location + * will be centered at the top of the View. + */ + public static final int TOP_CENTER = 2; + /** + * Special value for the alignment requested by a View. TOP_RIGHT means that the location + * will at the top right the View. + */ + public static final int TOP_RIGHT = 3; + /** + * Special value for the alignment requested by a View. CENTER_LEFT means that the location + * will at the center left the View. + */ + public static final int CENTER_LEFT = 4; + /** + * Special value for the alignment requested by a View. CENTER means that the location will + * be centered at the center of the View. + */ + public static final int CENTER = 5; + /** + * Special value for the alignment requested by a View. CENTER_RIGHT means that the location + * will at the center right the View. + */ + public static final int CENTER_RIGHT = 6; + /** + * Special value for the alignment requested by a View. BOTTOM_LEFT means that the location + * will be at the bottom left of the View. + */ + public static final int BOTTOM_LEFT = 7; + /** + * Special value for the alignment requested by a View. BOTTOM_CENTER means that the + * location will be centered at the bottom of the view. + */ + public static final int BOTTOM_CENTER = 8; + /** + * Special value for the alignment requested by a View. BOTTOM_RIGHT means that the location + * will be at the bottom right of the View. + */ + public static final int BOTTOM_RIGHT = 9; + /** + * The location of the child within the map view. + */ + public IGeoPoint geoPoint; + + /** + * The alignment the alignment of the view compared to the location. + */ + public int alignment; + + public int offsetX; + public int offsetY; + + /** + * Creates a new set of layout parameters with the specified width, height and location. + * + * @param width + * the width, either {@link #FILL_PARENT}, {@link #WRAP_CONTENT} or a fixed size + * in pixels + * @param height + * the height, either {@link #FILL_PARENT}, {@link #WRAP_CONTENT} or a fixed size + * in pixels + * @param geoPoint + * the location of the child within the map view + * @param alignment + * the alignment of the view compared to the location {@link #BOTTOM_CENTER}, + * {@link #BOTTOM_LEFT}, {@link #BOTTOM_RIGHT} {@link #TOP_CENTER}, + * {@link #TOP_LEFT}, {@link #TOP_RIGHT} + * @param offsetX + * the additional X offset from the alignment location to draw the child within + * the map view + * @param offsetY + * the additional Y offset from the alignment location to draw the child within + * the map view + */ + public LayoutParams(final int width, final int height, final IGeoPoint geoPoint, + final int alignment, final int offsetX, final int offsetY) { + super(width, height); + if (geoPoint != null) { + this.geoPoint = geoPoint; + } else { + this.geoPoint = new GeoPoint(0, 0); + } + this.alignment = alignment; + this.offsetX = offsetX; + this.offsetY = offsetY; + } + + /** + * Since we cannot use XML files in this project this constructor is useless. Creates a new + * set of layout parameters. The values are extracted from the supplied attributes set and + * context. + * + * @param c + * the application environment + * @param attrs + * the set of attributes fom which to extract the layout parameters values + */ + public LayoutParams(final Context c, final AttributeSet attrs) { + super(c, attrs); + this.geoPoint = new GeoPoint(0, 0); + this.alignment = BOTTOM_CENTER; + } + + /** + * {@inheritDoc} + */ + public LayoutParams(final ViewGroup.LayoutParams source) { + super(source); + } + } + +} diff --git a/src/main/java/org/osmdroid/views/overlay/DirectedLocationOverlay.java b/src/main/java/org/osmdroid/views/overlay/DirectedLocationOverlay.java new file mode 100644 index 000000000..dc2676334 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/DirectedLocationOverlay.java @@ -0,0 +1,159 @@ +// Created by plusminus on 22:01:11 - 29.09.2008 +package org.osmdroid.views.overlay; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Point; + +/** + * + * @author Nicolas Gramlich + * + */ +public class DirectedLocationOverlay extends Overlay { + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected final Paint mPaint = new Paint(); + protected final Paint mAccuracyPaint = new Paint(); + + protected final Bitmap DIRECTION_ARROW; + + protected GeoPoint mLocation; + protected float mBearing; + + private final Matrix directionRotater = new Matrix(); + private final Point screenCoords = new Point(); + + private final float DIRECTION_ARROW_CENTER_X; + private final float DIRECTION_ARROW_CENTER_Y; + private final int DIRECTION_ARROW_WIDTH; + private final int DIRECTION_ARROW_HEIGHT; + + private int mAccuracy = 0; + private boolean mShowAccuracy = true; + + // =========================================================== + // Constructors + // =========================================================== + + public DirectedLocationOverlay(final Context ctx) { + this(ctx, new DefaultResourceProxyImpl(ctx)); + } + + public DirectedLocationOverlay(final Context ctx, + final ResourceProxy pResourceProxy) { + super(pResourceProxy); + this.DIRECTION_ARROW = mResourceProxy.getBitmap(ResourceProxy.bitmap.direction_arrow); + + this.DIRECTION_ARROW_CENTER_X = this.DIRECTION_ARROW.getWidth() / 2 - 0.5f; + this.DIRECTION_ARROW_CENTER_Y = this.DIRECTION_ARROW.getHeight() / 2 - 0.5f; + this.DIRECTION_ARROW_HEIGHT = this.DIRECTION_ARROW.getHeight(); + this.DIRECTION_ARROW_WIDTH = this.DIRECTION_ARROW.getWidth(); + + this.mAccuracyPaint.setStrokeWidth(2); + this.mAccuracyPaint.setColor(Color.BLUE); + this.mAccuracyPaint.setAntiAlias(true); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public void setShowAccuracy(final boolean pShowIt) { + this.mShowAccuracy = pShowIt; + } + + public void setLocation(final GeoPoint mp) { + this.mLocation = mp; + } + + public GeoPoint getLocation() { + return this.mLocation; + } + + /** + * + * @param pAccuracy + * in Meters + */ + public void setAccuracy(final int pAccuracy) { + this.mAccuracy = pAccuracy; + } + + public void setBearing(final float aHeading) { + this.mBearing = aHeading; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public void draw(final Canvas c, final MapView osmv, final boolean shadow) { + + if (shadow) { + return; + } + + if (this.mLocation != null) { + final Projection pj = osmv.getProjection(); + pj.toMapPixels(this.mLocation, screenCoords); + + if (this.mShowAccuracy && this.mAccuracy > 10) { + final float accuracyRadius = pj.metersToEquatorPixels(this.mAccuracy); + /* Only draw if the DirectionArrow doesn't cover it. */ + if (accuracyRadius > 8) { + /* Draw the inner shadow. */ + this.mAccuracyPaint.setAntiAlias(false); + this.mAccuracyPaint.setAlpha(30); + this.mAccuracyPaint.setStyle(Style.FILL); + c.drawCircle(screenCoords.x, screenCoords.y, accuracyRadius, + this.mAccuracyPaint); + + /* Draw the edge. */ + this.mAccuracyPaint.setAntiAlias(true); + this.mAccuracyPaint.setAlpha(150); + this.mAccuracyPaint.setStyle(Style.STROKE); + c.drawCircle(screenCoords.x, screenCoords.y, accuracyRadius, + this.mAccuracyPaint); + } + } + + /* + * Rotate the direction-Arrow according to the bearing we are driving. And draw it to + * the canvas. + */ + this.directionRotater.setRotate(this.mBearing, DIRECTION_ARROW_CENTER_X, + DIRECTION_ARROW_CENTER_Y); + final Bitmap rotatedDirection = Bitmap.createBitmap(DIRECTION_ARROW, 0, 0, + DIRECTION_ARROW_WIDTH, DIRECTION_ARROW_HEIGHT, this.directionRotater, false); + c.drawBitmap(rotatedDirection, screenCoords.x - rotatedDirection.getWidth() / 2, + screenCoords.y - rotatedDirection.getHeight() / 2, this.mPaint); + } + } + + // =========================================================== + // Methods + // =========================================================== + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/views/overlay/IOverlayMenuProvider.java b/src/main/java/org/osmdroid/views/overlay/IOverlayMenuProvider.java new file mode 100644 index 000000000..fc593a135 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/IOverlayMenuProvider.java @@ -0,0 +1,26 @@ +package org.osmdroid.views.overlay; + +import org.osmdroid.views.MapView; + +import android.view.Menu; +import android.view.MenuItem; + +public interface IOverlayMenuProvider { + public boolean onCreateOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView); + + public boolean onPrepareOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView); + + public boolean onOptionsItemSelected(final MenuItem pItem, final int pMenuIdOffset, + final MapView pMapView); + + /** + * Can be used to signal to external callers that this Overlay should not be used for providing + * option menu items. + * + */ + public boolean isOptionsMenuEnabled(); + + public void setOptionsMenuEnabled(final boolean pOptionsMenuEnabled); +} diff --git a/src/main/java/org/osmdroid/views/overlay/ItemizedIconOverlay.java b/src/main/java/org/osmdroid/views/overlay/ItemizedIconOverlay.java new file mode 100644 index 000000000..8c460fa4a --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/ItemizedIconOverlay.java @@ -0,0 +1,217 @@ +package org.osmdroid.views.overlay; + +import java.util.List; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.ResourceProxy.bitmap; +import org.osmdroid.api.IMapView; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.view.MotionEvent; + +public class ItemizedIconOverlay extends ItemizedOverlay { + + protected final List mItemList; + protected OnItemGestureListener mOnItemGestureListener; + private int mDrawnItemsLimit = Integer.MAX_VALUE; + private final Point mTouchScreenPoint = new Point(); + private final Point mItemPoint = new Point(); + + public ItemizedIconOverlay( + final List pList, + final Drawable pDefaultMarker, + final org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener pOnItemGestureListener, + final ResourceProxy pResourceProxy) { + super(pDefaultMarker, pResourceProxy); + + this.mItemList = pList; + this.mOnItemGestureListener = pOnItemGestureListener; + populate(); + } + + public ItemizedIconOverlay( + final List pList, + final org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener pOnItemGestureListener, + final ResourceProxy pResourceProxy) { + this(pList, pResourceProxy.getDrawable(bitmap.marker_default), pOnItemGestureListener, + pResourceProxy); + } + + public ItemizedIconOverlay( + final Context pContext, + final List pList, + final org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener pOnItemGestureListener) { + this(pList, new DefaultResourceProxyImpl(pContext).getDrawable(bitmap.marker_default), + pOnItemGestureListener, new DefaultResourceProxyImpl(pContext)); + } + + @Override + public boolean onSnapToItem(final int pX, final int pY, final Point pSnapPoint, final IMapView pMapView) { + // TODO Implement this! + return false; + } + + @Override + protected Item createItem(final int index) { + return mItemList.get(index); + } + + @Override + public int size() { + return Math.min(mItemList.size(), mDrawnItemsLimit); + } + + public boolean addItem(final Item item) { + final boolean result = mItemList.add(item); + populate(); + return result; + } + + public void addItem(final int location, final Item item) { + mItemList.add(location, item); + populate(); + } + + public boolean addItems(final List items) { + final boolean result = mItemList.addAll(items); + populate(); + return result; + } + + public void removeAllItems() { + removeAllItems(true); + } + + public void removeAllItems(final boolean withPopulate) { + mItemList.clear(); + if (withPopulate) { + populate(); + } + } + + public boolean removeItem(final Item item) { + final boolean result = mItemList.remove(item); + populate(); + return result; + } + + public Item removeItem(final int position) { + final Item result = mItemList.remove(position); + populate(); + return result; + } + + /** + * Each of these methods performs a item sensitive check. If the item is located its + * corresponding method is called. The result of the call is returned. + * + * Helper methods are provided so that child classes may more easily override behavior without + * resorting to overriding the ItemGestureListener methods. + */ + @Override + public boolean onSingleTapConfirmed(final MotionEvent event, final MapView mapView) { + return (activateSelectedItems(event, mapView, new ActiveItem() { + @Override + public boolean run(final int index) { + final ItemizedIconOverlay that = ItemizedIconOverlay.this; + if (that.mOnItemGestureListener == null) { + return false; + } + return onSingleTapUpHelper(index, that.mItemList.get(index), mapView); + } + })) ? true : super.onSingleTapConfirmed(event, mapView); + } + + protected boolean onSingleTapUpHelper(final int index, final Item item, final MapView mapView) { + return this.mOnItemGestureListener.onItemSingleTapUp(index, item); + } + + @Override + public boolean onLongPress(final MotionEvent event, final MapView mapView) { + return (activateSelectedItems(event, mapView, new ActiveItem() { + @Override + public boolean run(final int index) { + final ItemizedIconOverlay that = ItemizedIconOverlay.this; + if (that.mOnItemGestureListener == null) { + return false; + } + return onLongPressHelper(index, getItem(index)); + } + })) ? true : super.onLongPress(event, mapView); + } + + protected boolean onLongPressHelper(final int index, final Item item) { + return this.mOnItemGestureListener.onItemLongPress(index, item); + } + + /** + * When a content sensitive action is performed the content item needs to be identified. This + * method does that and then performs the assigned task on that item. + * + * @param event + * @param mapView + * @param task + * @return true if event is handled false otherwise + */ + private boolean activateSelectedItems(final MotionEvent event, final MapView mapView, + final ActiveItem task) { + final Projection pj = mapView.getProjection(); + final int eventX = (int) event.getX(); + final int eventY = (int) event.getY(); + + /* These objects are created to avoid construct new ones every cycle. */ + pj.fromMapPixels(eventX, eventY, mTouchScreenPoint); + + for (int i = 0; i < this.mItemList.size(); ++i) { + final Item item = getItem(i); + final Drawable marker = (item.getMarker(0) == null) ? this.mDefaultMarker : item + .getMarker(0); + + pj.toPixels(item.getPoint(), mItemPoint); + + if (hitTest(item, marker, mTouchScreenPoint.x - mItemPoint.x, mTouchScreenPoint.y + - mItemPoint.y)) { + if (task.run(i)) { + return true; + } + } + } + return false; + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public int getDrawnItemsLimit() { + return this.mDrawnItemsLimit; + } + + public void setDrawnItemsLimit(final int aLimit) { + this.mDrawnItemsLimit = aLimit; + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + /** + * When the item is touched one of these methods may be invoked depending on the type of touch. + * + * Each of them returns true if the event was completely handled. + */ + public static interface OnItemGestureListener { + public boolean onItemSingleTapUp(final int index, final T item); + + public boolean onItemLongPress(final int index, final T item); + } + + public static interface ActiveItem { + public boolean run(final int aIndex); + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/ItemizedOverlay.java b/src/main/java/org/osmdroid/views/overlay/ItemizedOverlay.java new file mode 100644 index 000000000..9d95aed05 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/ItemizedOverlay.java @@ -0,0 +1,350 @@ +// Created by plusminus on 23:18:23 - 02.10.2008 +package org.osmdroid.views.overlay; + +import java.util.ArrayList; + +import org.osmdroid.ResourceProxy; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; +import org.osmdroid.views.overlay.OverlayItem.HotspotPlace; +import org.osmdroid.views.safecanvas.ISafeCanvas; +import org.osmdroid.views.safecanvas.ISafeCanvas.UnsafeCanvasHandler; + +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.MotionEvent; + +/** + * Draws a list of {@link OverlayItem} as markers to a map. The item with the lowest index is drawn + * as last and therefore the 'topmost' marker. It also gets checked for onTap first. This class is + * generic, because you then you get your custom item-class passed back in onTap(). + * + * @author Marc Kurtz + * @author Nicolas Gramlich + * @author Theodore Hong + * @author Fred Eisele + * + * @param + */ +public abstract class ItemizedOverlay extends SafeDrawOverlay implements + Overlay.Snappable { + + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected final Drawable mDefaultMarker; + private final ArrayList mInternalItemList; + private final Rect mRect = new Rect(); + private final Point mCurScreenCoords = new Point(); + protected boolean mDrawFocusedItem = true; + private Item mFocusedItem; + private boolean mPendingFocusChangedEvent = false; + private OnFocusChangeListener mOnFocusChangeListener; + + // =========================================================== + // Abstract methods + // =========================================================== + + /** + * Method by which subclasses create the actual Items. This will only be called from populate() + * we'll cache them for later use. + */ + protected abstract Item createItem(int i); + + /** + * The number of items in this overlay. + */ + public abstract int size(); + + // =========================================================== + // Constructors + // =========================================================== + + public ItemizedOverlay(final Drawable pDefaultMarker, final ResourceProxy pResourceProxy) { + + super(pResourceProxy); + + if (pDefaultMarker == null) { + throw new IllegalArgumentException("You must pass a default marker to ItemizedOverlay."); + } + + this.mDefaultMarker = pDefaultMarker; + + mInternalItemList = new ArrayList(); + } + + // =========================================================== + // Methods from SuperClass/Interfaces (and supporting methods) + // =========================================================== + + /** + * Draw a marker on each of our items. populate() must have been called first.
+ *
+ * The marker will be drawn twice for each Item in the Overlay--once in the shadow phase, skewed + * and darkened, then again in the non-shadow phase. The bottom-center of the marker will be + * aligned with the geographical coordinates of the Item.
+ *
+ * The order of drawing may be changed by overriding the getIndexToDraw(int) method. An item may + * provide an alternate marker via its OverlayItem.getMarker(int) method. If that method returns + * null, the default marker is used.
+ *
+ * The focused item is always drawn last, which puts it visually on top of the other items.
+ * + * @param canvas + * the Canvas upon which to draw. Note that this may already have a transformation + * applied, so be sure to leave it the way you found it + * @param mapView + * the MapView that requested the draw. Use MapView.getProjection() to convert + * between on-screen pixels and latitude/longitude pairs + * @param shadow + * if true, draw the shadow layer. If false, draw the overlay contents. + */ + @Override + protected void drawSafe(ISafeCanvas canvas, MapView mapView, boolean shadow) { + + if (shadow) { + return; + } + + if (mPendingFocusChangedEvent && mOnFocusChangeListener != null) + mOnFocusChangeListener.onFocusChanged(this, mFocusedItem); + mPendingFocusChangedEvent = false; + + final Projection pj = mapView.getProjection(); + final int size = this.mInternalItemList.size() - 1; + + /* Draw in backward cycle, so the items with the least index are on the front. */ + for (int i = size; i >= 0; i--) { + final Item item = getItem(i); + pj.toMapPixels(item.getPoint(), mCurScreenCoords); + + onDrawItem(canvas, item, mCurScreenCoords, mapView.getMapOrientation()); + } + } + + // =========================================================== + // Methods + // =========================================================== + + /** + * Utility method to perform all processing on a new ItemizedOverlay. Subclasses provide Items + * through the createItem(int) method. The subclass should call this as soon as it has data, + * before anything else gets called. + */ + protected final void populate() { + final int size = size(); + mInternalItemList.clear(); + mInternalItemList.ensureCapacity(size); + for (int a = 0; a < size; a++) { + mInternalItemList.add(createItem(a)); + } + } + + /** + * Returns the Item at the given index. + * + * @param position + * the position of the item to return + * @return the Item of the given index. + */ + public final Item getItem(final int position) { + return mInternalItemList.get(position); + } + + /** + * Draws an item located at the provided screen coordinates to the canvas. + * + * @param canvas + * what the item is drawn upon + * @param item + * the item to be drawn + * @param curScreenCoords + * @param aMapOrientation + */ + protected void onDrawItem(final ISafeCanvas canvas, final Item item, final Point curScreenCoords, final float aMapOrientation) { + final int state = (mDrawFocusedItem && (mFocusedItem == item) ? OverlayItem.ITEM_STATE_FOCUSED_MASK + : 0); + final Drawable marker = (item.getMarker(state) == null) ? getDefaultMarker(state) : item + .getMarker(state); + final HotspotPlace hotspot = item.getMarkerHotspot(); + + boundToHotspot(marker, hotspot); + + // draw it + if (this.isUsingSafeCanvas()) { + Overlay.drawAt(canvas.getSafeCanvas(), marker, curScreenCoords.x, curScreenCoords.y, false, aMapOrientation); + } else { + canvas.getUnsafeCanvas(new UnsafeCanvasHandler() { + @Override + public void onUnsafeCanvas(Canvas canvas) { + Overlay.drawAt(canvas, marker, curScreenCoords.x, curScreenCoords.y, false, aMapOrientation); + } + }); + } + } + + protected Drawable getDefaultMarker(final int state) { + OverlayItem.setState(mDefaultMarker, state); + return mDefaultMarker; + } + + /** + * See if a given hit point is within the bounds of an item's marker. Override to modify the way + * an item is hit tested. The hit point is relative to the marker's bounds. The default + * implementation just checks to see if the hit point is within the touchable bounds of the + * marker. + * + * @param item + * the item to hit test + * @param marker + * the item's marker + * @param hitX + * x coordinate of point to check + * @param hitY + * y coordinate of point to check + * @return true if the hit point is within the marker + */ + protected boolean hitTest(final Item item, final android.graphics.drawable.Drawable marker, final int hitX, + final int hitY) { + return marker.getBounds().contains(hitX, hitY); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e, MapView mapView) { + final Projection pj = mapView.getProjection(); + final Rect screenRect = pj.getIntrinsicScreenRect(); + final int size = this.size(); + + for (int i = 0; i < size; i++) { + final Item item = getItem(i); + pj.toMapPixels(item.getPoint(), mCurScreenCoords); + + final int state = (mDrawFocusedItem && (mFocusedItem == item) ? OverlayItem.ITEM_STATE_FOCUSED_MASK + : 0); + final Drawable marker = (item.getMarker(state) == null) ? getDefaultMarker(state) + : item.getMarker(state); + boundToHotspot(marker, item.getMarkerHotspot()); + if (hitTest(item, marker, -mCurScreenCoords.x + screenRect.left + (int) e.getX(), + -mCurScreenCoords.y + screenRect.top + (int) e.getY())) { + // We have a hit, do we get a response from onTap? + if (onTap(i)) { + // We got a response so consume the event + return true; + } + } + } + + return super.onSingleTapConfirmed(e, mapView); + } + + /** + * Override this method to handle a "tap" on an item. This could be from a touchscreen tap on an + * onscreen Item, or from a trackball click on a centered, selected Item. By default, does + * nothing and returns false. + * + * @return true if you handled the tap, false if you want the event that generated it to pass to + * other overlays. + */ + protected boolean onTap(int index) { + return false; + } + + /** + * Set whether or not to draw the focused item. The default is to draw it, but some clients may + * prefer to draw the focused item themselves. + */ + public void setDrawFocusedItem(final boolean drawFocusedItem) { + mDrawFocusedItem = drawFocusedItem; + } + + /** + * If the given Item is found in the overlay, force it to be the current focus-bearer. Any + * registered {@link ItemizedOverlay#OnFocusChangeListener} will be notified. This does not move + * the map, so if the Item isn't already centered, the user may get confused. If the Item is not + * found, this is a no-op. You can also pass null to remove focus. + */ + public void setFocus(final Item item) { + mPendingFocusChangedEvent = item != mFocusedItem; + mFocusedItem = item; + } + + /** + * + * @return the currently-focused item, or null if no item is currently focused. + */ + public Item getFocus() { + return mFocusedItem; + } + + /** + * Adjusts a drawable's bounds so that (0,0) is a pixel in the location described by the hotspot + * parameter. Useful for "pin"-like graphics. For convenience, returns the same drawable that + * was passed in. + * + * @param marker + * the drawable to adjust + * @param hotspot + * the hotspot for the drawable + * @return the same drawable that was passed in. + */ + protected synchronized Drawable boundToHotspot(final Drawable marker, HotspotPlace hotspot) { + final int markerWidth = marker.getIntrinsicWidth(); + final int markerHeight = marker.getIntrinsicHeight(); + + mRect.set(0, 0, 0 + markerWidth, 0 + markerHeight); + + if (hotspot == null) { + hotspot = HotspotPlace.BOTTOM_CENTER; + } + + switch (hotspot) { + default: + case NONE: + break; + case CENTER: + mRect.offset(-markerWidth / 2, -markerHeight / 2); + break; + case BOTTOM_CENTER: + mRect.offset(-markerWidth / 2, -markerHeight); + break; + case TOP_CENTER: + mRect.offset(-markerWidth / 2, 0); + break; + case RIGHT_CENTER: + mRect.offset(-markerWidth, -markerHeight / 2); + break; + case LEFT_CENTER: + mRect.offset(0, -markerHeight / 2); + break; + case UPPER_RIGHT_CORNER: + mRect.offset(-markerWidth, 0); + break; + case LOWER_RIGHT_CORNER: + mRect.offset(-markerWidth, -markerHeight); + break; + case UPPER_LEFT_CORNER: + mRect.offset(0, 0); + break; + case LOWER_LEFT_CORNER: + mRect.offset(0, -markerHeight); + break; + } + marker.setBounds(mRect); + return marker; + } + + public void setOnFocusChangeListener(OnFocusChangeListener l) { + mOnFocusChangeListener = l; + } + + public static interface OnFocusChangeListener { + void onFocusChanged(ItemizedOverlay overlay, OverlayItem newFocus); + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/ItemizedOverlayControlView.java b/src/main/java/org/osmdroid/views/overlay/ItemizedOverlayControlView.java new file mode 100644 index 000000000..776e25243 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/ItemizedOverlayControlView.java @@ -0,0 +1,148 @@ +// Created by plusminus on 22:59:38 - 12.09.2008 +package org.osmdroid.views.overlay; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +public class ItemizedOverlayControlView extends LinearLayout { + + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected ImageButton mPreviousButton; + protected ImageButton mNextButton; + protected ImageButton mCenterToButton; + protected ImageButton mNavToButton; + + protected ItemizedOverlayControlViewListener mLis; + + // =========================================================== + // Constructors + // =========================================================== + + public ItemizedOverlayControlView(final Context context, + final AttributeSet attrs) { + this(context, attrs, new DefaultResourceProxyImpl(context)); + } + + public ItemizedOverlayControlView(final Context context, + final AttributeSet attrs, final ResourceProxy pResourceProxy) { + super(context, attrs); + + this.mPreviousButton = new ImageButton(context); + this.mPreviousButton + .setImageBitmap(pResourceProxy.getBitmap(ResourceProxy.bitmap.previous)); + + this.mNextButton = new ImageButton(context); + this.mNextButton.setImageBitmap(pResourceProxy.getBitmap(ResourceProxy.bitmap.next)); + + this.mCenterToButton = new ImageButton(context); + this.mCenterToButton.setImageBitmap(pResourceProxy.getBitmap(ResourceProxy.bitmap.center)); + + this.mNavToButton = new ImageButton(context); + this.mNavToButton + .setImageBitmap(pResourceProxy.getBitmap(ResourceProxy.bitmap.navto_small)); + + this.addView(mPreviousButton, new LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + this.addView(mCenterToButton, new LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + this.addView(mNavToButton, new LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + this.addView(mNextButton, new LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + + initViewListeners(); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public void setItemizedOverlayControlViewListener(final ItemizedOverlayControlViewListener lis) { + this.mLis = lis; + } + + public void setNextEnabled(final boolean pEnabled) { + this.mNextButton.setEnabled(pEnabled); + } + + public void setPreviousEnabled(final boolean pEnabled) { + this.mPreviousButton.setEnabled(pEnabled); + } + + public void setNavToVisible(final int pVisibility) { + this.mNavToButton.setVisibility(pVisibility); + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + private void initViewListeners() { + this.mNextButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View v) { + if (ItemizedOverlayControlView.this.mLis != null) + ItemizedOverlayControlView.this.mLis.onNext(); + } + }); + + this.mPreviousButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View v) { + if (ItemizedOverlayControlView.this.mLis != null) + ItemizedOverlayControlView.this.mLis.onPrevious(); + } + }); + + this.mCenterToButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View v) { + if (ItemizedOverlayControlView.this.mLis != null) + ItemizedOverlayControlView.this.mLis.onCenter(); + } + }); + + this.mNavToButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View v) { + if (ItemizedOverlayControlView.this.mLis != null) + ItemizedOverlayControlView.this.mLis.onNavTo(); + } + }); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + public interface ItemizedOverlayControlViewListener { + public void onPrevious(); + + public void onNext(); + + public void onCenter(); + + public void onNavTo(); + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/ItemizedOverlayWithFocus.java b/src/main/java/org/osmdroid/views/overlay/ItemizedOverlayWithFocus.java new file mode 100644 index 000000000..b95d89259 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/ItemizedOverlayWithFocus.java @@ -0,0 +1,277 @@ +// Created by plusminus on 20:50:06 - 03.10.2008 +package org.osmdroid.views.overlay; + +import java.util.List; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.OverlayItem.HotspotPlace; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +public class ItemizedOverlayWithFocus extends ItemizedIconOverlay { + + // =========================================================== + // Constants + // =========================================================== + + public static final int DESCRIPTION_BOX_PADDING = 3; + public static final int DESCRIPTION_BOX_CORNERWIDTH = 3; + + public static final int DESCRIPTION_LINE_HEIGHT = 12; + /** Additional to DESCRIPTION_LINE_HEIGHT. */ + public static final int DESCRIPTION_TITLE_EXTRA_LINE_HEIGHT = 2; + + // protected static final Point DEFAULTMARKER_FOCUSED_HOTSPOT = new Point(10, 19); + protected static final int DEFAULTMARKER_BACKGROUNDCOLOR = Color.rgb(101, 185, 74); + + protected static final int DESCRIPTION_MAXWIDTH = 200; + + // =========================================================== + // Fields + // =========================================================== + + protected final int mMarkerFocusedBackgroundColor; + protected final Paint mMarkerBackgroundPaint, mDescriptionPaint, mTitlePaint; + + protected Drawable mMarkerFocusedBase; + protected int mFocusedItemIndex; + protected boolean mFocusItemsOnTap; + private final Point mFocusedScreenCoords = new Point(); + + private final String UNKNOWN; + + // =========================================================== + // Constructors + // =========================================================== + + public ItemizedOverlayWithFocus(final Context ctx, final List aList, + final OnItemGestureListener aOnItemTapListener) { + this(aList, aOnItemTapListener, new DefaultResourceProxyImpl(ctx)); + } + + public ItemizedOverlayWithFocus(final List aList, + final OnItemGestureListener aOnItemTapListener, final ResourceProxy pResourceProxy) { + this(aList, pResourceProxy.getDrawable(ResourceProxy.bitmap.marker_default), null, NOT_SET, + aOnItemTapListener, pResourceProxy); + } + + public ItemizedOverlayWithFocus(final List aList, final Drawable pMarker, + final Drawable pMarkerFocused, final int pFocusedBackgroundColor, + final OnItemGestureListener aOnItemTapListener, final ResourceProxy pResourceProxy) { + + super(aList, pMarker, aOnItemTapListener, pResourceProxy); + + UNKNOWN = mResourceProxy.getString(ResourceProxy.string.unknown); + + if (pMarkerFocused == null) { + this.mMarkerFocusedBase = boundToHotspot( + mResourceProxy.getDrawable(ResourceProxy.bitmap.marker_default_focused_base), + HotspotPlace.BOTTOM_CENTER); + } else + this.mMarkerFocusedBase = pMarkerFocused; + + this.mMarkerFocusedBackgroundColor = (pFocusedBackgroundColor != NOT_SET) ? pFocusedBackgroundColor + : DEFAULTMARKER_BACKGROUNDCOLOR; + + this.mMarkerBackgroundPaint = new Paint(); // Color is set in onDraw(...) + + this.mDescriptionPaint = new Paint(); + this.mDescriptionPaint.setAntiAlias(true); + this.mTitlePaint = new Paint(); + this.mTitlePaint.setFakeBoldText(true); + this.mTitlePaint.setAntiAlias(true); + this.unSetFocusedItem(); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public Item getFocusedItem() { + if (this.mFocusedItemIndex == NOT_SET) { + return null; + } + return this.mItemList.get(this.mFocusedItemIndex); + } + + public void setFocusedItem(final int pIndex) { + this.mFocusedItemIndex = pIndex; + } + + public void unSetFocusedItem() { + this.mFocusedItemIndex = NOT_SET; + } + + public void setFocusedItem(final Item pItem) { + final int indexFound = super.mItemList.indexOf(pItem); + if (indexFound < 0) { + throw new IllegalArgumentException(); + } + + this.setFocusedItem(indexFound); + } + + public void setFocusItemsOnTap(final boolean doit) { + this.mFocusItemsOnTap = doit; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + protected boolean onSingleTapUpHelper(final int index, final Item item, final MapView mapView) { + if (this.mFocusItemsOnTap) { + this.mFocusedItemIndex = index; + mapView.postInvalidate(); + } + return this.mOnItemGestureListener.onItemSingleTapUp(index, item); + } + + private final Rect mRect = new Rect(); + + @Override + public void draw(final Canvas c, final MapView osmv, final boolean shadow) { + + super.draw(c, osmv, shadow); + + if (shadow) { + return; + } + + if (this.mFocusedItemIndex == NOT_SET) { + return; + } + + // get focused item's preferred marker & hotspot + final Item focusedItem = super.mItemList.get(this.mFocusedItemIndex); + Drawable markerFocusedBase = focusedItem.getMarker(OverlayItem.ITEM_STATE_FOCUSED_MASK); + if (markerFocusedBase == null) { + markerFocusedBase = this.mMarkerFocusedBase; + } + + /* Calculate and set the bounds of the marker. */ + osmv.getProjection().toMapPixels(focusedItem.getPoint(), mFocusedScreenCoords); + + markerFocusedBase.copyBounds(mRect); + mRect.offset(mFocusedScreenCoords.x, mFocusedScreenCoords.y); + + /* Strings of the OverlayItem, we need. */ + final String itemTitle = (focusedItem.getTitle() == null) ? UNKNOWN : focusedItem + .getTitle(); + final String itemDescription = (focusedItem.getSnippet() == null) ? UNKNOWN : focusedItem + .getSnippet(); + + /* + * Store the width needed for each char in the description to a float array. This is pretty + * efficient. + */ + final float[] widths = new float[itemDescription.length()]; + this.mDescriptionPaint.getTextWidths(itemDescription, widths); + + final StringBuilder sb = new StringBuilder(); + int maxWidth = 0; + int curLineWidth = 0; + int lastStop = 0; + int i; + int lastwhitespace = 0; + /* + * Loop through the charwidth array and harshly insert a linebreak, when the width gets + * bigger than DESCRIPTION_MAXWIDTH. + */ + for (i = 0; i < widths.length; i++) { + if (!Character.isLetter(itemDescription.charAt(i))) { + lastwhitespace = i; + } + + final float charwidth = widths[i]; + + if (curLineWidth + charwidth > DESCRIPTION_MAXWIDTH) { + if (lastStop == lastwhitespace) { + i--; + } else { + i = lastwhitespace; + } + + sb.append(itemDescription.subSequence(lastStop, i)); + sb.append('\n'); + + lastStop = i; + maxWidth = Math.max(maxWidth, curLineWidth); + curLineWidth = 0; + } + + curLineWidth += charwidth; + } + /* Add the last line to the rest to the buffer. */ + if (i != lastStop) { + final String rest = itemDescription.substring(lastStop, i); + maxWidth = Math.max(maxWidth, (int) this.mDescriptionPaint.measureText(rest)); + sb.append(rest); + } + final String[] lines = sb.toString().split("\n"); + + /* + * The title also needs to be taken into consideration for the width calculation. + */ + final int titleWidth = (int) this.mDescriptionPaint.measureText(itemTitle); + + maxWidth = Math.max(maxWidth, titleWidth); + final int descWidth = Math.min(maxWidth, DESCRIPTION_MAXWIDTH); + + /* Calculate the bounds of the Description box that needs to be drawn. */ + final int descBoxLeft = mRect.left - descWidth / 2 - DESCRIPTION_BOX_PADDING + + mRect.width() / 2; + final int descBoxRight = descBoxLeft + descWidth + 2 * DESCRIPTION_BOX_PADDING; + final int descBoxBottom = mRect.top; + final int descBoxTop = descBoxBottom - DESCRIPTION_TITLE_EXTRA_LINE_HEIGHT + - (lines.length + 1) * DESCRIPTION_LINE_HEIGHT /* +1 because of the title. */ + - 2 * DESCRIPTION_BOX_PADDING; + + /* Twice draw a RoundRect, once in black with 1px as a small border. */ + this.mMarkerBackgroundPaint.setColor(Color.BLACK); + c.drawRoundRect(new RectF(descBoxLeft - 1, descBoxTop - 1, descBoxRight + 1, + descBoxBottom + 1), DESCRIPTION_BOX_CORNERWIDTH, DESCRIPTION_BOX_CORNERWIDTH, + this.mDescriptionPaint); + this.mMarkerBackgroundPaint.setColor(this.mMarkerFocusedBackgroundColor); + c.drawRoundRect(new RectF(descBoxLeft, descBoxTop, descBoxRight, descBoxBottom), + DESCRIPTION_BOX_CORNERWIDTH, DESCRIPTION_BOX_CORNERWIDTH, + this.mMarkerBackgroundPaint); + + final int descLeft = descBoxLeft + DESCRIPTION_BOX_PADDING; + int descTextLineBottom = descBoxBottom - DESCRIPTION_BOX_PADDING; + + /* Draw all the lines of the description. */ + for (int j = lines.length - 1; j >= 0; j--) { + c.drawText(lines[j].trim(), descLeft, descTextLineBottom, this.mDescriptionPaint); + descTextLineBottom -= DESCRIPTION_LINE_HEIGHT; + } + /* Draw the title. */ + c.drawText(itemTitle, descLeft, descTextLineBottom - DESCRIPTION_TITLE_EXTRA_LINE_HEIGHT, + this.mTitlePaint); + c.drawLine(descBoxLeft, descTextLineBottom, descBoxRight, descTextLineBottom, + mDescriptionPaint); + + /* + * Finally draw the marker base. This is done in the end to make it look better. + */ + Overlay.drawAt(c, markerFocusedBase, mFocusedScreenCoords.x, mFocusedScreenCoords.y, false, osmv.getMapOrientation()); + } + + // =========================================================== + // Methods + // =========================================================== + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/views/overlay/MinimapOverlay.java b/src/main/java/org/osmdroid/views/overlay/MinimapOverlay.java new file mode 100644 index 000000000..10f4603c3 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/MinimapOverlay.java @@ -0,0 +1,310 @@ +package org.osmdroid.views.overlay; + +import org.osmdroid.tileprovider.MapTileProviderBase; +import org.osmdroid.tileprovider.MapTileProviderBasic; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.util.TileSystem; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; +import org.osmdroid.views.safecanvas.ISafeCanvas; +import org.osmdroid.views.safecanvas.SafePaint; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.view.MotionEvent; + +/** + * Draws a mini-map as an overlay layer. It currently uses its own MapTileProviderBasic or a tile + * provider supplied to it. Do NOT share a tile provider amongst multiple tile drawing overlays - it + * will create an under-sized cache. + * + * @author Marc Kurtz + * + */ +public class MinimapOverlay extends TilesOverlay { + + private int mWidth = 100; + private int mHeight = 100; + private int mPadding = 10; + private int mZoomDifference; + private final SafePaint mPaint; + private int mWorldSize_2; + + // The Mercator coordinates of what is on the screen + final private Rect mViewportRect = new Rect(); + + // The Mercator coordinates of the map tile area we are interested in the target zoom level + final private Rect mTileArea = new Rect(); + + // The Canvas coordinates where the minimap should be drawn + final private Rect mMiniMapCanvasRect = new Rect(); + + // Stores the intersection of the minimap and the Canvas clipping area + final private Rect mIntersectionRect = new Rect(); + + /** + * Creates a {@link MinimapOverlay} with the supplied tile provider. The {@link Handler} passed + * in is typically the same handler being used by the main map. The {@link MapTileProviderBase} + * passed in cannot be the same tile provider used in the {@link TilesOverlay}, it must be a new + * instance. + * + * @param pContext + * a context + * @param tileRequestCompleteHandler + * a handler for the tile request complete notifications + * @param pTileProvider + * a tile provider + */ + public MinimapOverlay(final Context pContext, final Handler pTileRequestCompleteHandler, + final MapTileProviderBase pTileProvider, final int pZoomDifference) { + super(pTileProvider, pContext); + setZoomDifference(pZoomDifference); + + mTileProvider.setTileRequestCompleteHandler(pTileRequestCompleteHandler); + + // Don't draw loading lines in the minimap + setLoadingLineColor(getLoadingBackgroundColor()); + + // Scale the default size + final float density = pContext.getResources().getDisplayMetrics().density; + mWidth *= density; + mHeight *= density; + + mPaint = new SafePaint(); + mPaint.setColor(Color.GRAY); + mPaint.setStyle(Style.FILL); + mPaint.setStrokeWidth(2); + } + + /** + * Creates a {@link MinimapOverlay} with the supplied tile provider. The {@link Handler} passed + * in is typically the same handler being used by the main map. The {@link MapTileProviderBase} + * passed in cannot be the same tile provider used in the {@link TilesOverlay}, it must be a new + * instance. + * + * @param pContext + * a context + * @param tileRequestCompleteHandler + * a handler for the tile request complete notifications + * @param pTileProvider + * a tile provider + */ + public MinimapOverlay(final Context pContext, final Handler pTileRequestCompleteHandler, + final MapTileProviderBase pTileProvider) { + this(pContext, pTileRequestCompleteHandler, pTileProvider, + DEFAULT_ZOOMLEVEL_MINIMAP_DIFFERENCE); + } + + /** + * Creates a {@link MinimapOverlay} that uses its own {@link MapTileProviderBasic}. The + * {@link Handler} passed in is typically the same handler being used by the main map. + * + * @param pContext + * a context + * @param tileRequestCompleteHandler + * a handler for tile request complete notifications + */ + public MinimapOverlay(final Context pContext, final Handler pTileRequestCompleteHandler) { + this(pContext, pTileRequestCompleteHandler, new MapTileProviderBasic(pContext)); + } + + public void setTileSource(final ITileSource pTileSource) { + mTileProvider.setTileSource(pTileSource); + } + + public int getZoomDifference() { + return mZoomDifference; + } + + public void setZoomDifference(final int zoomDifference) { + mZoomDifference = zoomDifference; + } + + @Override + protected void drawSafe(final ISafeCanvas pC, final MapView pOsmv, final boolean shadow) { + + if (shadow) { + return; + } + + // Don't draw if we are animating + if (pOsmv.isAnimating()) { + return; + } + + // Calculate the half-world size + final Projection projection = pOsmv.getProjection(); + final int zoomLevel = projection.getZoomLevel(); + mWorldSize_2 = TileSystem.MapSize(zoomLevel) / 2; + + // Save the Mercator coordinates of what is on the screen + mViewportRect.set(projection.getScreenRect()); + mViewportRect.offset(mWorldSize_2, mWorldSize_2); + + // Start calculating the tile area with the current viewport + mTileArea.set(mViewportRect); + + // Get the target zoom level difference. + int miniMapZoomLevelDifference = getZoomDifference(); + + // Make sure the zoom level difference isn't below the minimum zoom level + if (zoomLevel - getZoomDifference() < mTileProvider.getMinimumZoomLevel()) { + miniMapZoomLevelDifference += zoomLevel - getZoomDifference() + - mTileProvider.getMinimumZoomLevel(); + } + + // Shift the screen coordinates into the target zoom level + mTileArea.set(mTileArea.left >> miniMapZoomLevelDifference, + mTileArea.top >> miniMapZoomLevelDifference, + mTileArea.right >> miniMapZoomLevelDifference, + mTileArea.bottom >> miniMapZoomLevelDifference); + + // Limit the area we are interested in for tiles to be the MAP_WIDTH by MAP_HEIGHT and + // centered on the center of the screen + mTileArea.set(mTileArea.centerX() - (getWidth() / 2), mTileArea.centerY() + - (getHeight() / 2), mTileArea.centerX() + (getWidth() / 2), mTileArea.centerY() + + (getHeight() / 2)); + + // Get the area where we will draw the minimap in screen coordinates + mMiniMapCanvasRect.set(mViewportRect.right - getPadding() - getWidth(), + mViewportRect.bottom - getPadding() - getHeight(), mViewportRect.right + - getPadding(), mViewportRect.bottom - getPadding()); + mMiniMapCanvasRect.offset(-mWorldSize_2, -mWorldSize_2); + + // Draw a solid background where the minimap will be drawn with a 2 pixel inset + pC.drawRect(mMiniMapCanvasRect.left - 2, mMiniMapCanvasRect.top - 2, + mMiniMapCanvasRect.right + 2, mMiniMapCanvasRect.bottom + 2, mPaint); + + super.drawTiles(pC.getSafeCanvas(), projection.getZoomLevel() - miniMapZoomLevelDifference, + TileSystem.getTileSize(), mTileArea); + } + + @Override + protected void onTileReadyToDraw(final Canvas c, final Drawable currentMapTile, + final Rect tileRect) { + + // Get the offsets for where to draw the tiles relative to where the minimap is located + final int xOffset = (tileRect.left - mTileArea.left) + (mMiniMapCanvasRect.left); + final int yOffset = (tileRect.top - mTileArea.top) + (mMiniMapCanvasRect.top); + + // Set the drawable's location + currentMapTile.setBounds(xOffset, yOffset, xOffset + tileRect.width(), + yOffset + tileRect.height()); + + // Save the current clipping bounds + final Rect oldClip = c.getClipBounds(); + + // Check to see if the drawing area intersects with the minimap area + if (mIntersectionRect.setIntersect(oldClip, mMiniMapCanvasRect)) { + // If so, then clip that area + c.clipRect(mIntersectionRect); + + // Draw the tile, which will be appropriately clipped + currentMapTile.draw(c); + + // Restore the original clipping bounds + c.clipRect(oldClip); + } + } + + @Override + public boolean onSingleTapUp(final MotionEvent pEvent, final MapView pMapView) { + // Consume event so layers underneath don't receive + if (mMiniMapCanvasRect.contains((int) pEvent.getX() + mViewportRect.left - mWorldSize_2, + (int) pEvent.getY() + mViewportRect.top - mWorldSize_2)) { + return true; + } + + return false; + } + + @Override + public boolean onDoubleTap(final MotionEvent pEvent, final MapView pMapView) { + // Consume event so layers underneath don't receive + if (mMiniMapCanvasRect.contains((int) pEvent.getX() + mViewportRect.left - mWorldSize_2, + (int) pEvent.getY() + mViewportRect.top - mWorldSize_2)) { + return true; + } + + return false; + } + + @Override + public boolean onLongPress(final MotionEvent pEvent, final MapView pMapView) { + // Consume event so layers underneath don't receive + if (mMiniMapCanvasRect.contains((int) pEvent.getX() + mViewportRect.left - mWorldSize_2, + (int) pEvent.getY() + mViewportRect.top - mWorldSize_2)) { + return true; + } + + return false; + } + + @Override + public boolean isOptionsMenuEnabled() { + // Don't provide menu items from TilesOverlay. + return false; + } + + /** + * Sets the width of the mini-map in pixels + * + * @param width + * the width to set in pixels + */ + public void setWidth(final int width) { + mWidth = width; + } + + /** + * Gets the width of the mini-map in pixels + * + * @return the width in pixels + */ + public int getWidth() { + return mWidth; + } + + /** + * Sets the height of the mini-map in pixels + * + * @param height + * the height to set in pixels + */ + public void setHeight(final int height) { + mHeight = height; + } + + /** + * Gets the height of the mini-map in pixels + * + * @return the height in pixels + */ + public int getHeight() { + return mHeight; + } + + /** + * Sets the number of pixels from the lower-right corner to offset the mini-map + * + * @param padding + * the padding to set in pixels + */ + public void setPadding(final int padding) { + mPadding = padding; + } + + /** + * Gets the number of pixels from the lower-right corner to offset the mini-map + * + * @return the padding in pixels + */ + public int getPadding() { + return mPadding; + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/MyLocationOverlay.java b/src/main/java/org/osmdroid/views/overlay/MyLocationOverlay.java new file mode 100644 index 000000000..075fe8ede --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/MyLocationOverlay.java @@ -0,0 +1,881 @@ +// Created by plusminus on 22:01:11 - 29.09.2008 +package org.osmdroid.views.overlay; + +import java.util.LinkedList; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.LocationListenerProxy; +import org.osmdroid.ResourceProxy; +import org.osmdroid.SensorEventListenerProxy; +import org.osmdroid.api.IMapController; +import org.osmdroid.api.IMapView; +import org.osmdroid.api.IMyLocationOverlay; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.util.LocationUtils; +import org.osmdroid.util.NetworkLocationIgnorer; +import org.osmdroid.util.TileSystem; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; +import org.osmdroid.views.overlay.Overlay.Snappable; +import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; +import org.osmdroid.views.util.constants.MapViewConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.view.Display; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.WindowManager; + +/** + * + * @author Manuel Stahl + * + * @deprecated Use {@link MyLocationNewOverlay} instead. + */ +public class MyLocationOverlay extends Overlay implements IMyLocationOverlay, IOverlayMenuProvider, + SensorEventListener, LocationListener, Snappable { + + private static final Logger logger = LoggerFactory.getLogger(MyLocationOverlay.class); + + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected final Paint mPaint = new Paint(); + protected final Paint mCirclePaint = new Paint(); + + protected final Bitmap PERSON_ICON; + protected final Bitmap DIRECTION_ARROW; + + protected final MapView mMapView; + + private final IMapController mMapController; + private final LocationManager mLocationManager; + private final SensorManager mSensorManager; + private final Display mDisplay; + + public LocationListenerProxy mLocationListener = null; + public SensorEventListenerProxy mSensorListener = null; + + private final LinkedList mRunOnFirstFix = new LinkedList(); + private final Point mMapCoords = new Point(); + + private Location mLocation; + private final GeoPoint mGeoPoint = new GeoPoint(0, 0); // for reuse + private long mLocationUpdateMinTime = 0; + private float mLocationUpdateMinDistance = 0.0f; + protected boolean mFollow = false; // follow location updates + protected boolean mDrawAccuracyEnabled = true; + private final NetworkLocationIgnorer mIgnorer = new NetworkLocationIgnorer(); + + private final Matrix directionRotater = new Matrix(); + + /** Coordinates the feet of the person are located scaled for display density. */ + protected final PointF PERSON_HOTSPOT; + + protected final float DIRECTION_ARROW_CENTER_X; + protected final float DIRECTION_ARROW_CENTER_Y; + + protected final Picture mCompassFrame = new Picture(); + protected final Picture mCompassRose = new Picture(); + private final Matrix mCompassMatrix = new Matrix(); + + /** + * The bearing, in degrees east of north, or NaN if none has been set. + */ + private float mAzimuth = Float.NaN; + + private float mCompassCenterX = 35.0f; + private float mCompassCenterY = 35.0f; + private final float mCompassRadius = 20.0f; + + protected final float COMPASS_FRAME_CENTER_X; + protected final float COMPASS_FRAME_CENTER_Y; + protected final float COMPASS_ROSE_CENTER_X; + protected final float COMPASS_ROSE_CENTER_Y; + + public static final int MENU_MY_LOCATION = getSafeMenuId(); + public static final int MENU_COMPASS = getSafeMenuId(); + + private boolean mOptionsMenuEnabled = true; + + // to avoid allocations during onDraw + private final float[] mMatrixValues = new float[9]; + private final Matrix mMatrix = new Matrix(); + private final Rect mMyLocationRect = new Rect(); + private final Rect mMyLocationPreviousRect = new Rect(); + + // =========================================================== + // Constructors + // =========================================================== + + public MyLocationOverlay(final Context ctx, final MapView mapView) { + this(ctx, mapView, new DefaultResourceProxyImpl(ctx)); + } + + public MyLocationOverlay(final Context ctx, final MapView mapView, + final ResourceProxy pResourceProxy) { + super(pResourceProxy); + mMapView = mapView; + mLocationManager = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE); + mSensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE); + final WindowManager windowManager = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE); + mDisplay = windowManager.getDefaultDisplay(); + + mMapController = mapView.getController(); + mCirclePaint.setARGB(0, 100, 100, 255); + mCirclePaint.setAntiAlias(true); + + PERSON_ICON = mResourceProxy.getBitmap(ResourceProxy.bitmap.person); + DIRECTION_ARROW = mResourceProxy.getBitmap(ResourceProxy.bitmap.direction_arrow); + + DIRECTION_ARROW_CENTER_X = DIRECTION_ARROW.getWidth() / 2 - 0.5f; + DIRECTION_ARROW_CENTER_Y = DIRECTION_ARROW.getHeight() / 2 - 0.5f; + + // Calculate position of person icon's feet, scaled to screen density + PERSON_HOTSPOT = new PointF(24.0f * mScale + 0.5f, 39.0f * mScale + 0.5f); + + createCompassFramePicture(); + createCompassRosePicture(); + + COMPASS_FRAME_CENTER_X = mCompassFrame.getWidth() / 2 - 0.5f; + COMPASS_FRAME_CENTER_Y = mCompassFrame.getHeight() / 2 - 0.5f; + COMPASS_ROSE_CENTER_X = mCompassRose.getWidth() / 2 - 0.5f; + COMPASS_ROSE_CENTER_Y = mCompassRose.getHeight() / 2 - 0.5f; + } + + private void invalidateCompass() { + Rect screenRect = mMapView.getProjection().getScreenRect(); + final int frameLeft = screenRect.left + (mMapView.getWidth() / 2) + + (int) Math.ceil((mCompassCenterX - COMPASS_FRAME_CENTER_X) * mScale); + final int frameTop = screenRect.top + (mMapView.getHeight() / 2) + + (int) Math.ceil((mCompassCenterY - COMPASS_FRAME_CENTER_Y) * mScale); + final int frameRight = screenRect.left + (mMapView.getWidth() / 2) + + (int) Math.ceil((mCompassCenterX + COMPASS_FRAME_CENTER_X) * mScale); + final int frameBottom = screenRect.top + (mMapView.getHeight() / 2) + + (int) Math.ceil((mCompassCenterY + COMPASS_FRAME_CENTER_Y) * mScale); + + // Offset by 2 to cover stroke width + mMapView.postInvalidate(frameLeft - 2, frameTop - 2, frameRight + 2, frameBottom + 2); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public long getLocationUpdateMinTime() { + return mLocationUpdateMinTime; + } + + /** + * Set the minimum interval for location updates. + * See {@link LocationManager#requestLocationUpdates(String, long, float, LocationListener)}. + * Note that you should call this before calling {@link #enableMyLocation()}. + * + * @param milliSeconds + */ + public void setLocationUpdateMinTime(final long milliSeconds) { + mLocationUpdateMinTime = milliSeconds; + } + + public float getLocationUpdateMinDistance() { + return mLocationUpdateMinDistance; + } + + /** + * Set the minimum distance for location updates. + * See {@link LocationManager#requestLocationUpdates}. + * Note that you should call this before calling {@link #enableMyLocation()}. + * + * @param meters + */ + public void setLocationUpdateMinDistance(final float meters) { + mLocationUpdateMinDistance = meters; + } + + public void setCompassCenter(final float x, final float y) { + mCompassCenterX = x; + mCompassCenterY = y; + } + + /** + * If enabled, an accuracy circle will be drawn around your current position. + * + * @param drawAccuracyEnabled + * whether the accuracy circle will be enabled + */ + public void setDrawAccuracyEnabled(final boolean drawAccuracyEnabled) { + mDrawAccuracyEnabled = drawAccuracyEnabled; + } + + /** + * If enabled, an accuracy circle will be drawn around your current position. + * + * @return true if enabled, false otherwise + */ + public boolean isDrawAccuracyEnabled() { + return mDrawAccuracyEnabled; + } + + protected void drawMyLocation(final Canvas canvas, + final MapView mapView, + final Location lastFix) { + + final Projection pj = mapView.getProjection(); + final int zoomDiff = MapViewConstants.MAXIMUM_ZOOMLEVEL - pj.getZoomLevel(); + + if (mDrawAccuracyEnabled) { + final float radius = lastFix.getAccuracy() / (float) TileSystem.GroundResolution(lastFix.getLatitude(), mapView.getZoomLevel()); + + mCirclePaint.setAlpha(50); + mCirclePaint.setStyle(Style.FILL); + canvas.drawCircle(mMapCoords.x >> zoomDiff, mMapCoords.y >> zoomDiff, radius, + mCirclePaint); + + mCirclePaint.setAlpha(150); + mCirclePaint.setStyle(Style.STROKE); + canvas.drawCircle(mMapCoords.x >> zoomDiff, mMapCoords.y >> zoomDiff, radius, + mCirclePaint); + } + + canvas.getMatrix(mMatrix); + mMatrix.getValues(mMatrixValues); + + if (DEBUGMODE) { + final float tx = (-mMatrixValues[Matrix.MTRANS_X] + 20) + / mMatrixValues[Matrix.MSCALE_X]; + final float ty = (-mMatrixValues[Matrix.MTRANS_Y] + 90) + / mMatrixValues[Matrix.MSCALE_Y]; + canvas.drawText("Lat: " + lastFix.getLatitude(), tx, ty + 5, mPaint); + canvas.drawText("Lon: " + lastFix.getLongitude(), tx, ty + 20, mPaint); + canvas.drawText("Alt: " + lastFix.getAltitude(), tx, ty + 35, mPaint); + canvas.drawText("Acc: " + lastFix.getAccuracy(), tx, ty + 50, mPaint); + } + + if (lastFix.hasBearing()) { + /* + * Rotate the direction-Arrow according to the bearing we are driving. And draw it + * to the canvas. + */ + directionRotater.setRotate( + lastFix.getBearing(), + DIRECTION_ARROW_CENTER_X, DIRECTION_ARROW_CENTER_Y); + + directionRotater.postTranslate(-DIRECTION_ARROW_CENTER_X, -DIRECTION_ARROW_CENTER_Y); + directionRotater.postScale( + 1 / mMatrixValues[Matrix.MSCALE_X], + 1 / mMatrixValues[Matrix.MSCALE_Y]); + directionRotater.postTranslate(mMapCoords.x >> zoomDiff, mMapCoords.y >> zoomDiff); + canvas.drawBitmap(DIRECTION_ARROW, directionRotater, mPaint); + } else { + directionRotater.setTranslate(-PERSON_HOTSPOT.x, -PERSON_HOTSPOT.y); + directionRotater.postScale( + 1 / mMatrixValues[Matrix.MSCALE_X], + 1 / mMatrixValues[Matrix.MSCALE_Y]); + directionRotater.postTranslate(mMapCoords.x >> zoomDiff, mMapCoords.y >> zoomDiff); + canvas.drawBitmap(PERSON_ICON, directionRotater, mPaint); + } + } + + protected Rect getMyLocationDrawingBounds(int zoomLevel, Location lastFix, Rect reuse) { + if (reuse == null) + reuse = new Rect(); + + final int zoomDiff = MapViewConstants.MAXIMUM_ZOOMLEVEL - zoomLevel; + final int posX = mMapCoords.x >> zoomDiff; + final int posY = mMapCoords.y >> zoomDiff; + + // Start with the bitmap bounds + if (lastFix.hasBearing()) { + // Get a square bounding box around the object, and expand by the length of the diagonal + // so as to allow for extra space for rotating + int widestEdge = (int) Math.ceil(Math.max(DIRECTION_ARROW.getWidth(), + DIRECTION_ARROW.getHeight()) + * Math.sqrt(2)); + reuse.set(posX, posY, posX + widestEdge, posY + widestEdge); + reuse.offset((int) -widestEdge / 2, (int) -widestEdge / 2); + } else { + reuse.set(posX, posY, posX + PERSON_ICON.getWidth(), posY + PERSON_ICON.getHeight()); + reuse.offset((int) -PERSON_HOTSPOT.x, (int) -PERSON_HOTSPOT.y); + } + + // Add in the accuracy circle if enabled + if (mDrawAccuracyEnabled) { + final int radius = (int) Math.ceil(lastFix.getAccuracy() / (float) TileSystem.GroundResolution(lastFix.getLatitude(), zoomLevel)); + reuse.union(posX - radius, posY - radius, posX + radius, posY + radius); + final int strokeWidth = (int) Math.ceil(mCirclePaint.getStrokeWidth() == 0 ? 1 + : mCirclePaint.getStrokeWidth()); + reuse.inset(-strokeWidth, -strokeWidth); + } + + reuse.offset(mMapView.getWidth() / 2, mMapView.getHeight() / 2); + + return reuse; + } + + protected void drawCompass(final Canvas canvas, final float bearing, final Rect screenRect) { + final float centerX = mCompassCenterX * mScale; + final float centerY = mCompassCenterY * mScale + (canvas.getHeight() - mMapView.getHeight()); + + mCompassMatrix.setTranslate(-COMPASS_FRAME_CENTER_X, -COMPASS_FRAME_CENTER_Y); + mCompassMatrix.postTranslate(centerX, centerY); + + canvas.save(); + canvas.setMatrix(mCompassMatrix); + canvas.drawPicture(mCompassFrame); + + mCompassMatrix.setRotate(-bearing, COMPASS_ROSE_CENTER_X, COMPASS_ROSE_CENTER_Y); + mCompassMatrix.postTranslate(-COMPASS_ROSE_CENTER_X, -COMPASS_ROSE_CENTER_Y); + mCompassMatrix.postTranslate(centerX, centerY); + + canvas.setMatrix(mCompassMatrix); + canvas.drawPicture(mCompassRose); + canvas.restore(); + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public void draw(final Canvas canvas, final MapView mapView, final boolean shadow) { + + if (shadow) { + return; + } + + if (mLocation != null) { + drawMyLocation(canvas, mapView, mLocation); + } + + if (isCompassEnabled() && !Float.isNaN(mAzimuth)) { + drawCompass(canvas, mAzimuth + getDisplayOrientation(), mapView.getProjection() + .getScreenRect()); + } + } + + @Override + public void onLocationChanged(final Location location) { + if (DEBUGMODE) { + logger.debug("onLocationChanged(" + location + ")"); + } + + // ignore temporary non-gps fix + if (mIgnorer.shouldIgnore(location.getProvider(), System.currentTimeMillis())) { + logger.debug("Ignore temporary non-gps location"); + return; + } + + // If we had a previous location, let's get those bounds + Location oldLocation = mLocation; + if (oldLocation != null) { + this.getMyLocationDrawingBounds(mMapView.getZoomLevel(), oldLocation, + mMyLocationPreviousRect); + } + + mLocation = location; + TileSystem.LatLongToPixelXY(location.getLatitude(), location.getLongitude(), MapViewConstants.MAXIMUM_ZOOMLEVEL, mMapCoords); + final int worldSize_2 = TileSystem.MapSize(MapViewConstants.MAXIMUM_ZOOMLEVEL) / 2; + mMapCoords.offset(-worldSize_2, -worldSize_2); + + if (mFollow) { + mGeoPoint.setLatitudeE6((int) (mLocation.getLatitude() * 1E6)); + mGeoPoint.setLongitudeE6((int) (mLocation.getLongitude() * 1E6)); + mMapController.animateTo(mGeoPoint); + } else { + if (mLocation != null) { + // Get new drawing bounds + this.getMyLocationDrawingBounds(mMapView.getZoomLevel(), mLocation, mMyLocationRect); + + // If we had a previous location, merge in those bounds too + if (oldLocation != null) { + mMyLocationRect.union(mMyLocationPreviousRect); + } + + // Invalidate the bounds + mMapView.postInvalidate(mMyLocationRect.left, mMyLocationRect.top, + mMyLocationRect.right, mMyLocationRect.bottom); + } + } + + for (final Runnable runnable : mRunOnFirstFix) { + new Thread(runnable).start(); + } + mRunOnFirstFix.clear(); + } + + @Override + public void onProviderDisabled(final String provider) { + } + + @Override + public void onProviderEnabled(final String provider) { + } + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) { + } + + @Override + public boolean onSnapToItem(final int x, final int y, final Point snapPoint, + final IMapView mapView) { + if (this.mLocation != null) { + snapPoint.x = mMapCoords.x; + snapPoint.y = mMapCoords.y; + final double xDiff = x - mMapCoords.x; + final double yDiff = y - mMapCoords.y; + final boolean snap = xDiff * xDiff + yDiff * yDiff < 64; + if (DEBUGMODE) { + logger.debug("snap=" + snap); + } + return snap; + } else { + return false; + } + } + + @Override + public boolean onTouchEvent(final MotionEvent event, final MapView mapView) { + if (event.getAction() == MotionEvent.ACTION_MOVE) { + disableFollowLocation(); + } + + return super.onTouchEvent(event, mapView); + } + + @Override + public void onAccuracyChanged(final Sensor arg0, final int arg1) { + // This is not interesting for us at the moment + } + + @Override + public void onSensorChanged(final SensorEvent event) { + if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { + if (event.values != null) { + mAzimuth = event.values[0]; + this.invalidateCompass(); + } + } + } + + // =========================================================== + // Menu handling methods + // =========================================================== + + @Override + public void setOptionsMenuEnabled(final boolean pOptionsMenuEnabled) { + this.mOptionsMenuEnabled = pOptionsMenuEnabled; + } + + @Override + public boolean isOptionsMenuEnabled() { + return this.mOptionsMenuEnabled; + } + + @Override + public boolean onCreateOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView) { + pMenu.add(0, MENU_MY_LOCATION + pMenuIdOffset, Menu.NONE, + mResourceProxy.getString(ResourceProxy.string.my_location)).setIcon( + mResourceProxy.getDrawable(ResourceProxy.bitmap.ic_menu_mylocation)); + + pMenu.add(0, MENU_COMPASS + pMenuIdOffset, Menu.NONE, + mResourceProxy.getString(ResourceProxy.string.compass)).setIcon( + mResourceProxy.getDrawable(ResourceProxy.bitmap.ic_menu_compass)); + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView) { + return false; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem pItem, final int pMenuIdOffset, + final MapView pMapView) { + final int menuId = pItem.getItemId() - pMenuIdOffset; + if (menuId == MENU_MY_LOCATION) { + if (this.isMyLocationEnabled()) { + this.disableFollowLocation(); + this.disableMyLocation(); + } else { + this.enableFollowLocation(); + this.enableMyLocation(); + } + return true; + } else if (menuId == MENU_COMPASS) { + if (this.isCompassEnabled()) { + this.disableCompass(); + } else { + this.enableCompass(); + } + return true; + } else { + return false; + } + } + + // =========================================================== + // Methods + // =========================================================== + + /** + * Return a GeoPoint of the last known location, or null if not known. + */ + public GeoPoint getMyLocation() { + if (mLocation == null) { + return null; + } else { + return new GeoPoint(mLocation); + } + } + + @Override + public Location getLastFix() { + return mLocation; + } + + /** + * @deprecated use {@link #enableFollowLocation()} and {@link #disableFollowLocation()} instead. + */ + @Deprecated + public void followLocation(final boolean follow) { + if (follow) { + enableFollowLocation(); + } else { + disableFollowLocation(); + } + } + + /** + * Enables "follow" functionality. The map will center on your current location and + * automatically scroll as you move. Scrolling the map in the UI will disable. + */ + public void enableFollowLocation() { + mFollow = true; + + // set initial location when enabled + if (isMyLocationEnabled()) { + mLocation = LocationUtils.getLastKnownLocation(mLocationManager); + if (mLocation != null) { + TileSystem.LatLongToPixelXY(mLocation.getLatitude(), mLocation.getLongitude(), MapViewConstants.MAXIMUM_ZOOMLEVEL, mMapCoords); + final int worldSize_2 = TileSystem.MapSize(MapViewConstants.MAXIMUM_ZOOMLEVEL) / 2; + mMapCoords.offset(-worldSize_2, -worldSize_2); + mMapController.animateTo(new GeoPoint(mLocation)); + } + } + + // Update the screen to see changes take effect + if (mMapView != null) { + mMapView.postInvalidate(); + } + } + + /** + * Disables "follow" functionality. + */ + public void disableFollowLocation() { + mFollow = false; + } + + /** + * If enabled, the map will center on your current location and automatically scroll as you + * move. Scrolling the map in the UI will disable. + * + * @return true if enabled, false otherwise + */ + public boolean isFollowLocationEnabled() { + return mFollow; + } + + /** + * Enable location updates and show your current location on the map. By default this will + * request location updates as frequently as possible, but you can change the frequency and/or + * distance by calling {@link #setLocationUpdateMinTime(long)} and/or {@link + * #setLocationUpdateMinDistance(float)} before calling this method. You will want to call + * enableMyLocation() probably from your Activity's Activity.onResume() method, to enable the + * features of this overlay. Remember to call the corresponding disableMyLocation() in your + * Activity's Activity.onPause() method to turn off updates when in the background. + */ + @Override + public boolean enableMyLocation() { + boolean result = true; + + if (mLocationListener == null) { + mLocationListener = new LocationListenerProxy(mLocationManager); + result = mLocationListener.startListening(this, mLocationUpdateMinTime, + mLocationUpdateMinDistance); + } + + // set initial location when enabled + if (isFollowLocationEnabled()) { + mLocation = LocationUtils.getLastKnownLocation(mLocationManager); + if (mLocation != null) { + TileSystem.LatLongToPixelXY(mLocation.getLatitude(), mLocation.getLongitude(), MapViewConstants.MAXIMUM_ZOOMLEVEL, mMapCoords); + final int worldSize_2 = TileSystem.MapSize(MapViewConstants.MAXIMUM_ZOOMLEVEL) / 2; + mMapCoords.offset(-worldSize_2, -worldSize_2); + mMapController.animateTo(new GeoPoint(mLocation)); + } + } + + // Update the screen to see changes take effect + if (mMapView != null) { + mMapView.postInvalidate(); + } + + return result; + } + + /** + * Disable location updates + */ + @Override + public void disableMyLocation() { + if (mLocationListener != null) { + mLocationListener.stopListening(); + } + + mLocationListener = null; + + // Update the screen to see changes take effect + if (mMapView != null) { + mMapView.postInvalidate(); + } + } + + /** + * If enabled, the map is receiving location updates and drawing your location on the map. + * + * @return true if enabled, false otherwise + */ + @Override + public boolean isMyLocationEnabled() { + return mLocationListener != null; + } + + /** + * Enable orientation sensor (compass) updates and show a compass on the map. You will want to + * call enableCompass() probably from your Activity's Activity.onResume() method, to enable the + * features of this overlay. Remember to call the corresponding disableCompass() in your + * Activity's Activity.onPause() method to turn off updates when in the background. + */ + @Override + public boolean enableCompass() { + boolean result = true; + if (mSensorListener == null) { + mSensorListener = new SensorEventListenerProxy(mSensorManager); + result = mSensorListener.startListening(this, Sensor.TYPE_ORIENTATION, + SensorManager.SENSOR_DELAY_UI); + } + + // Update the screen to see changes take effect + if (mMapView != null) { + this.invalidateCompass(); + } + + return result; + } + + /** + * Disable orientation updates + */ + @Override + public void disableCompass() { + if (mSensorListener != null) { + mSensorListener.stopListening(); + } + + // Reset values + mSensorListener = null; + mAzimuth = Float.NaN; + + // Update the screen to see changes take effect + if (mMapView != null) { + this.invalidateCompass(); + } + } + + /** + * If enabled, the map is receiving orientation updates and drawing your location on the map. + * + * @return true if enabled, false otherwise + */ + @Override + public boolean isCompassEnabled() { + return mSensorListener != null; + } + + @Override + public float getOrientation() { + return mAzimuth; + } + + @Override + public boolean runOnFirstFix(final Runnable runnable) { + if (mLocationListener != null && mLocation != null) { + new Thread(runnable).start(); + return true; + } else { + mRunOnFirstFix.addLast(runnable); + return false; + } + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + private Point calculatePointOnCircle(final float centerX, final float centerY, + final float radius, final float degrees) { + // for trigonometry, 0 is pointing east, so subtract 90 + // compass degrees are the wrong way round + final double dblRadians = Math.toRadians(-degrees + 90); + + final int intX = (int) (radius * Math.cos(dblRadians)); + final int intY = (int) (radius * Math.sin(dblRadians)); + + return new Point((int) centerX + intX, (int) centerY - intY); + } + + private void drawTriangle(final Canvas canvas, final float x, final float y, + final float radius, final float degrees, final Paint paint) { + canvas.save(); + final Point point = this.calculatePointOnCircle(x, y, radius, degrees); + canvas.rotate(degrees, point.x, point.y); + final Path p = new Path(); + p.moveTo(point.x - 2 * mScale, point.y); + p.lineTo(point.x + 2 * mScale, point.y); + p.lineTo(point.x, point.y - 5 * mScale); + p.close(); + canvas.drawPath(p, paint); + canvas.restore(); + } + + private int getDisplayOrientation() { + switch (mDisplay.getOrientation()) { + case Surface.ROTATION_90: return 90; + case Surface.ROTATION_180: return 180; + case Surface.ROTATION_270: return 270; + default: return 0; + } + } + + private void createCompassFramePicture() { + // The inside of the compass is white and transparent + final Paint innerPaint = new Paint(); + innerPaint.setColor(Color.WHITE); + innerPaint.setAntiAlias(true); + innerPaint.setStyle(Style.FILL); + innerPaint.setAlpha(200); + + // The outer part (circle and little triangles) is gray and transparent + final Paint outerPaint = new Paint(); + outerPaint.setColor(Color.GRAY); + outerPaint.setAntiAlias(true); + outerPaint.setStyle(Style.STROKE); + outerPaint.setStrokeWidth(2.0f); + outerPaint.setAlpha(200); + + final int picBorderWidthAndHeight = (int) ((mCompassRadius + 5) * 2); + final int center = picBorderWidthAndHeight / 2; + + final Canvas canvas = mCompassFrame.beginRecording(picBorderWidthAndHeight, + picBorderWidthAndHeight); + + // draw compass inner circle and border + canvas.drawCircle(center, center, mCompassRadius * mScale, innerPaint); + canvas.drawCircle(center, center, mCompassRadius * mScale, outerPaint); + + // Draw little triangles north, south, west and east (don't move) + // to make those move use "-bearing + 0" etc. (Note: that would mean to draw the triangles + // in the onDraw() method) + drawTriangle(canvas, center, center, mCompassRadius * mScale, 0, outerPaint); + drawTriangle(canvas, center, center, mCompassRadius * mScale, 90, outerPaint); + drawTriangle(canvas, center, center, mCompassRadius * mScale, 180, outerPaint); + drawTriangle(canvas, center, center, mCompassRadius * mScale, 270, outerPaint); + + mCompassFrame.endRecording(); + } + + private void createCompassRosePicture() { + // Paint design of north triangle (it's common to paint north in red color) + final Paint northPaint = new Paint(); + northPaint.setColor(0xFFA00000); + northPaint.setAntiAlias(true); + northPaint.setStyle(Style.FILL); + northPaint.setAlpha(220); + + // Paint design of south triangle (black) + final Paint southPaint = new Paint(); + southPaint.setColor(Color.BLACK); + southPaint.setAntiAlias(true); + southPaint.setStyle(Style.FILL); + southPaint.setAlpha(220); + + // Create a little white dot in the middle of the compass rose + final Paint centerPaint = new Paint(); + centerPaint.setColor(Color.WHITE); + centerPaint.setAntiAlias(true); + centerPaint.setStyle(Style.FILL); + centerPaint.setAlpha(220); + + // final int picBorderWidthAndHeight = (int) ((mCompassRadius + 5) * 2 * mScale); + final int picBorderWidthAndHeight = (int) ((mCompassRadius + 5) * 2); + final int center = picBorderWidthAndHeight / 2; + + final Canvas canvas = mCompassRose.beginRecording(picBorderWidthAndHeight, + picBorderWidthAndHeight); + + // Blue triangle pointing north + final Path pathNorth = new Path(); + pathNorth.moveTo(center, center - (mCompassRadius - 3) * mScale); + pathNorth.lineTo(center + 4 * mScale, center); + pathNorth.lineTo(center - 4 * mScale, center); + pathNorth.lineTo(center, center - (mCompassRadius - 3) * mScale); + pathNorth.close(); + canvas.drawPath(pathNorth, northPaint); + + // Red triangle pointing south + final Path pathSouth = new Path(); + pathSouth.moveTo(center, center + (mCompassRadius - 3) * mScale); + pathSouth.lineTo(center + 4 * mScale, center); + pathSouth.lineTo(center - 4 * mScale, center); + pathSouth.lineTo(center, center + (mCompassRadius - 3) * mScale); + pathSouth.close(); + canvas.drawPath(pathSouth, southPaint); + + // Draw a little white dot in the middle + canvas.drawCircle(center, center, 2, centerPaint); + + mCompassRose.endRecording(); + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/Overlay.java b/src/main/java/org/osmdroid/views/overlay/Overlay.java new file mode 100644 index 000000000..b6b24e8fd --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/Overlay.java @@ -0,0 +1,314 @@ +// Created by plusminus on 20:32:01 - 27.09.2008 +package org.osmdroid.views.overlay; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.api.IMapView; +import org.osmdroid.views.MapView; +import org.osmdroid.views.util.constants.OverlayConstants; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** + * Base class representing an overlay which may be displayed on top of a {@link MapView}. To add an + * overlay, subclass this class, create an instance, and add it to the list obtained from + * getOverlays() of {@link MapView}. + * + * This class implements a form of Gesture Handling similar to + * {@link android.view.GestureDetector.SimpleOnGestureListener} and + * {@link GestureDetector.OnGestureListener}. The difference is there is an additional argument for + * the item. + * + * @author Nicolas Gramlich + */ +public abstract class Overlay implements OverlayConstants { + + // =========================================================== + // Constants + // =========================================================== + + private static AtomicInteger sOrdinal = new AtomicInteger(); + + // From Google Maps API + protected static final float SHADOW_X_SKEW = -0.8999999761581421f; + protected static final float SHADOW_Y_SCALE = 0.5f; + + // =========================================================== + // Fields + // =========================================================== + + protected final ResourceProxy mResourceProxy; + protected final float mScale; + private static final Rect mRect = new Rect(); + private boolean mEnabled = true; + + // =========================================================== + // Constructors + // =========================================================== + + public Overlay(final Context ctx) { + mResourceProxy = new DefaultResourceProxyImpl(ctx); + mScale = ctx.getResources().getDisplayMetrics().density; + } + + public Overlay(final ResourceProxy pResourceProxy) { + mResourceProxy = pResourceProxy; + mScale = mResourceProxy.getDisplayMetricsDensity(); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + /** + * Sets whether the Overlay is marked to be enabled. This setting does nothing by default, but + * should be checked before calling draw(). + */ + public void setEnabled(final boolean pEnabled) { + this.mEnabled = pEnabled; + } + + /** + * Specifies if the Overlay is marked to be enabled. This should be checked before calling + * draw(). + * + * @return true if the Overlay is marked enabled, false otherwise + */ + public boolean isEnabled() { + return this.mEnabled; + } + + /** + * Since the menu-chain will pass through several independent Overlays, menu IDs cannot be fixed + * at compile time. Overlays should use this method to obtain and store a menu id for each menu + * item at construction time. This will ensure that two overlays don't use the same id. + * + * @return an integer suitable to be used as a menu identifier + */ + protected final static int getSafeMenuId() { + return sOrdinal.getAndIncrement(); + } + + /** + * Similar to , except this reserves a sequence of IDs of length + * . The returned number is the starting index of that sequential list. + * + * @return an integer suitable to be used as a menu identifier + */ + protected final static int getSafeMenuIdSequence(final int count) { + return sOrdinal.getAndAdd(count); + } + + // =========================================================== + // Methods for SuperClass/Interfaces + // =========================================================== + + /** + * Draw the overlay over the map. This will be called on all active overlays with shadow=true, + * to lay down the shadow layer, and then again on all overlays with shadow=false. Callers + * should check isEnabled() before calling draw(). By default, draws nothing. + */ + protected abstract void draw(final Canvas c, final MapView osmv, final boolean shadow); + + // =========================================================== + // Methods + // =========================================================== + + /** + * Override to perform clean up of resources before shutdown. By default does nothing. + */ + public void onDetach(final MapView mapView) { + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onKeyDown(final int keyCode, final KeyEvent event, final MapView mapView) { + return false; + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onKeyUp(final int keyCode, final KeyEvent event, final MapView mapView) { + return false; + } + + /** + * You can prevent all(!) other Touch-related events from happening!
+ * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onTouchEvent(final MotionEvent event, final MapView mapView) { + return false; + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onTrackballEvent(final MotionEvent event, final MapView mapView) { + return false; + } + + /** GestureDetector.OnDoubleTapListener **/ + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onDoubleTap(final MotionEvent e, final MapView mapView) { + return false; + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onDoubleTapEvent(final MotionEvent e, final MapView mapView) { + return false; + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onSingleTapConfirmed(final MotionEvent e, final MapView mapView) { + return false; + } + + /** OnGestureListener **/ + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onDown(final MotionEvent e, final MapView mapView) { + return false; + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onFling(final MotionEvent pEvent1, final MotionEvent pEvent2, + final float pVelocityX, final float pVelocityY, final MapView pMapView) { + return false; + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onLongPress(final MotionEvent e, final MapView mapView) { + return false; + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onScroll(final MotionEvent pEvent1, final MotionEvent pEvent2, + final float pDistanceX, final float pDistanceY, final MapView pMapView) { + return false; + } + + public void onShowPress(final MotionEvent pEvent, final MapView pMapView) { + return; + } + + /** + * By default does nothing (return false). If you handled the Event, return + * true, otherwise return false. If you returned true + * none of the following Overlays or the underlying {@link MapView} has the chance to handle + * this event. + */ + public boolean onSingleTapUp(final MotionEvent e, final MapView mapView) { + return false; + } + + /** + * Convenience method to draw a Drawable at an offset. x and y are pixel coordinates. You can + * find appropriate coordinates from latitude/longitude using the MapView.getProjection() method + * on the MapView passed to you in draw(Canvas, MapView, boolean). + * + * @param shadow + * If true, draw only the drawable's shadow. Otherwise, draw the drawable itself. + * @param aMapOrientation + */ + protected synchronized static void drawAt(final Canvas canvas, final Drawable drawable, + final int x, final int y, final boolean shadow, + final float aMapOrientation) { + canvas.save(); + canvas.rotate(-aMapOrientation, x, y); + drawable.copyBounds(mRect); + drawable.setBounds(mRect.left + x, mRect.top + y, mRect.right + x, mRect.bottom + y); + drawable.draw(canvas); + drawable.setBounds(mRect); + canvas.restore(); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + /** + * Interface definition for overlays that contain items that can be snapped to (for example, + * when the user invokes a zoom, this could be called allowing the user to snap the zoom to an + * interesting point.) + */ + public interface Snappable { + + /** + * Checks to see if the given x and y are close enough to an item resulting in snapping the + * current action (e.g. zoom) to the item. + * + * @param x + * The x in screen coordinates. + * @param y + * The y in screen coordinates. + * @param snapPoint + * To be filled with the the interesting point (in screen coordinates) that is + * closest to the given x and y. Can be untouched if not snapping. + * @param mapView + * The {@link MapView} that is requesting the snap. Use MapView.getProjection() + * to convert between on-screen pixels and latitude/longitude pairs. + * @return Whether or not to snap to the interesting point. + */ + boolean onSnapToItem(int x, int y, Point snapPoint, IMapView mapView); + } + +} diff --git a/src/main/java/org/osmdroid/views/overlay/OverlayItem.java b/src/main/java/org/osmdroid/views/overlay/OverlayItem.java new file mode 100644 index 000000000..79d828ddf --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/OverlayItem.java @@ -0,0 +1,167 @@ +// Created by plusminus on 00:02:58 - 03.10.2008 +package org.osmdroid.views.overlay; + +import org.osmdroid.util.GeoPoint; + +import android.graphics.Point; +import android.graphics.drawable.Drawable; + +/** + * Immutable class describing a GeoPoint with a Title and a Description. + * + * @author Nicolas Gramlich + * @author Theodore Hong + * @author Fred Eisele + * + */ +public class OverlayItem { + + // =========================================================== + // Constants + // =========================================================== + public static final int ITEM_STATE_FOCUSED_MASK = 4; + public static final int ITEM_STATE_PRESSED_MASK = 1; + public static final int ITEM_STATE_SELECTED_MASK = 2; + + protected static final Point DEFAULT_MARKER_SIZE = new Point(26, 94); + + /** + * Indicates a hotspot for an area. This is where the origin (0,0) of a point will be located + * relative to the area. In otherwords this acts as an offset. NONE indicates that no adjustment + * should be made. + */ + public enum HotspotPlace { + NONE, CENTER, BOTTOM_CENTER, TOP_CENTER, RIGHT_CENTER, LEFT_CENTER, UPPER_RIGHT_CORNER, LOWER_RIGHT_CORNER, UPPER_LEFT_CORNER, LOWER_LEFT_CORNER + } + + // =========================================================== + // Fields + // =========================================================== + + protected final String mUid; + protected final String mTitle; + protected final String mSnippet; + protected final GeoPoint mGeoPoint; + protected Drawable mMarker; + protected HotspotPlace mHotspotPlace; + + // =========================================================== + // Constructors + // =========================================================== + + /** + * @param aTitle + * this should be singleLine (no '\n' ) + * @param aSnippet + * a multiLine description ( '\n' possible) + * @param aGeoPoint + */ + public OverlayItem(final String aTitle, final String aSnippet, final GeoPoint aGeoPoint) { + this(null, aTitle, aSnippet, aGeoPoint); + } + + public OverlayItem(final String aUid, final String aTitle, final String aDescription, + final GeoPoint aGeoPoint) { + this.mTitle = aTitle; + this.mSnippet = aDescription; + this.mGeoPoint = aGeoPoint; + this.mUid = aUid; + } + + // =========================================================== + // Getter & Setter + // =========================================================== + public String getUid() { + return mUid; + } + + public String getTitle() { + return mTitle; + } + + public String getSnippet() { + return mSnippet; + } + + public GeoPoint getPoint() { + return mGeoPoint; + } + + /* + * (copied from Google API docs) Returns the marker that should be used when drawing this item + * on the map. A null value means that the default marker should be drawn. Different markers can + * be returned for different states. The different markers can have different bounds. The + * default behavior is to call {@link setState(android.graphics.drawable.Drawable, int)} on the + * overlay item's marker, if it exists, and then return it. + * + * @param stateBitset The current state. + * + * @return The marker for the current state, or null if the default marker for the overlay + * should be used. + */ + public Drawable getMarker(final int stateBitset) { + // marker not specified + if (mMarker == null) { + return null; + } + + // set marker state appropriately + setState(mMarker, stateBitset); + return mMarker; + } + + public void setMarker(final Drawable marker) { + this.mMarker = marker; + } + + public void setMarkerHotspot(final HotspotPlace place) { + this.mHotspotPlace = (place == null) ? HotspotPlace.BOTTOM_CENTER : place; + } + + public HotspotPlace getMarkerHotspot() { + return this.mHotspotPlace; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + /* + * (copied from the Google API docs) Sets the state of a drawable to match a given state bitset. + * This is done by converting the state bitset bits into a state set of R.attr.state_pressed, + * R.attr.state_selected and R.attr.state_focused attributes, and then calling {@link + * Drawable.setState(int[])}. + */ + public static void setState(final Drawable drawable, final int stateBitset) { + final int[] states = new int[3]; + int index = 0; + if ((stateBitset & ITEM_STATE_PRESSED_MASK) > 0) + states[index++] = android.R.attr.state_pressed; + if ((stateBitset & ITEM_STATE_SELECTED_MASK) > 0) + states[index++] = android.R.attr.state_selected; + if ((stateBitset & ITEM_STATE_FOCUSED_MASK) > 0) + states[index++] = android.R.attr.state_focused; + + drawable.setState(states); + } + + public Drawable getDrawable() { + return this.mMarker; + } + + public int getWidth() { + return this.mMarker.getIntrinsicWidth(); + } + + public int getHeight() { + return this.mMarker.getIntrinsicHeight(); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + +} diff --git a/src/main/java/org/osmdroid/views/overlay/OverlayManager.java b/src/main/java/org/osmdroid/views/overlay/OverlayManager.java new file mode 100644 index 000000000..69eb48647 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/OverlayManager.java @@ -0,0 +1,366 @@ +package org.osmdroid.views.overlay; + +import java.util.AbstractList; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.osmdroid.api.IMapView; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.Overlay.Snappable; + +import android.graphics.Canvas; +import android.graphics.Point; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; + +public class OverlayManager extends AbstractList { + + private TilesOverlay mTilesOverlay; + private boolean mUseSafeCanvas = true; + + private final CopyOnWriteArrayList mOverlayList; + + public OverlayManager(final TilesOverlay tilesOverlay) { + setTilesOverlay(tilesOverlay); + mOverlayList = new CopyOnWriteArrayList(); + } + + @Override + public Overlay get(final int pIndex) { + return mOverlayList.get(pIndex); + } + + @Override + public int size() { + return mOverlayList.size(); + } + + @Override + public void add(final int pIndex, final Overlay pElement) { + mOverlayList.add(pIndex, pElement); + if (pElement instanceof SafeDrawOverlay) + ((SafeDrawOverlay) pElement).setUseSafeCanvas(this.isUsingSafeCanvas()); + } + + @Override + public Overlay remove(final int pIndex) { + return mOverlayList.remove(pIndex); + } + + @Override + public Overlay set(final int pIndex, final Overlay pElement) { + Overlay overlay = mOverlayList.set(pIndex, pElement); + if (pElement instanceof SafeDrawOverlay) + ((SafeDrawOverlay) pElement).setUseSafeCanvas(this.isUsingSafeCanvas()); + return overlay; + } + + public boolean isUsingSafeCanvas() { + return mUseSafeCanvas; + } + + public void setUseSafeCanvas(boolean useSafeCanvas) { + mUseSafeCanvas = useSafeCanvas; + for (Overlay overlay : mOverlayList) + if (overlay instanceof SafeDrawOverlay) + ((SafeDrawOverlay) overlay).setUseSafeCanvas(this.isUsingSafeCanvas()); + if (mTilesOverlay != null) { + mTilesOverlay.setUseSafeCanvas(this.isUsingSafeCanvas()); + } + } + + /** + * Gets the optional TilesOverlay class. + * + * @return the tilesOverlay + */ + public TilesOverlay getTilesOverlay() { + return mTilesOverlay; + } + + /** + * Sets the optional TilesOverlay class. If set, this overlay will be drawn before all other + * overlays and will not be included in the editable list of overlays and can't be cleared + * except by a subsequent call to setTilesOverlay(). + * + * @param tilesOverlay the tilesOverlay to set + */ + public void setTilesOverlay(final TilesOverlay tilesOverlay) { + mTilesOverlay = tilesOverlay; + if (mTilesOverlay != null) { + mTilesOverlay.setUseSafeCanvas(this.isUsingSafeCanvas()); + } + } + + public Iterable overlaysReversed() { + return new Iterable() { + @Override + public Iterator iterator() { + final ListIterator i = mOverlayList.listIterator(mOverlayList.size()); + + return new Iterator() { + @Override + public boolean hasNext() { + return i.hasPrevious(); + } + + @Override + public Overlay next() { + return i.previous(); + } + + @Override + public void remove() { + i.remove(); + } + }; + } + }; + } + + public void onDraw(final Canvas c, final MapView pMapView) { + if (mTilesOverlay != null && mTilesOverlay.isEnabled()) { + mTilesOverlay.draw(c, pMapView, true); + } + + if (mTilesOverlay != null && mTilesOverlay.isEnabled()) { + mTilesOverlay.draw(c, pMapView, false); + } + + for (final Overlay overlay : mOverlayList) { + if (overlay.isEnabled()) { + overlay.draw(c, pMapView, true); + } + } + + for (final Overlay overlay : mOverlayList) { + if (overlay.isEnabled()) { + overlay.draw(c, pMapView, false); + } + } + + } + + public void onDetach(final MapView pMapView) { + if (mTilesOverlay != null) { + mTilesOverlay.onDetach(pMapView); + } + + for (final Overlay overlay : this.overlaysReversed()) { + overlay.onDetach(pMapView); + } + } + + public boolean onKeyDown(final int keyCode, final KeyEvent event, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onKeyDown(keyCode, event, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onKeyUp(final int keyCode, final KeyEvent event, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onKeyUp(keyCode, event, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onTouchEvent(final MotionEvent event, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onTouchEvent(event, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onTrackballEvent(final MotionEvent event, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onTrackballEvent(event, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onSnapToItem(final int x, final int y, final Point snapPoint, final IMapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay instanceof Snappable) { + if (((Snappable) overlay).onSnapToItem(x, y, snapPoint, pMapView)) { + return true; + } + } + } + + return false; + } + + /* GestureDetector.OnDoubleTapListener */ + + public boolean onDoubleTap(final MotionEvent e, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onDoubleTap(e, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onDoubleTapEvent(final MotionEvent e, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onDoubleTapEvent(e, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onSingleTapConfirmed(final MotionEvent e, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onSingleTapConfirmed(e, pMapView)) { + return true; + } + } + + return false; + } + + /* OnGestureListener */ + + public boolean onDown(final MotionEvent pEvent, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onDown(pEvent, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onFling(final MotionEvent pEvent1, final MotionEvent pEvent2, + final float pVelocityX, final float pVelocityY, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onFling(pEvent1, pEvent2, pVelocityX, pVelocityY, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onLongPress(final MotionEvent pEvent, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onLongPress(pEvent, pMapView)) { + return true; + } + } + + return false; + } + + public boolean onScroll(final MotionEvent pEvent1, final MotionEvent pEvent2, + final float pDistanceX, final float pDistanceY, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onScroll(pEvent1, pEvent2, pDistanceX, pDistanceY, pMapView)) { + return true; + } + } + + return false; + } + + public void onShowPress(final MotionEvent pEvent, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + overlay.onShowPress(pEvent, pMapView); + } + } + + public boolean onSingleTapUp(final MotionEvent pEvent, final MapView pMapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay.onSingleTapUp(pEvent, pMapView)) { + return true; + } + } + + return false; + } + + // ** Options Menu **// + + public void setOptionsMenusEnabled(final boolean pEnabled) { + for (final Overlay overlay : mOverlayList) { + if ((overlay instanceof IOverlayMenuProvider) + && ((IOverlayMenuProvider) overlay).isOptionsMenuEnabled()) { + ((IOverlayMenuProvider) overlay).setOptionsMenuEnabled(pEnabled); + } + } + } + + public boolean onCreateOptionsMenu(final Menu pMenu, final int menuIdOffset, final MapView mapView) { + boolean result = true; + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay instanceof IOverlayMenuProvider) { + final IOverlayMenuProvider overlayMenuProvider = (IOverlayMenuProvider) overlay; + if (overlayMenuProvider.isOptionsMenuEnabled()) { + result &= overlayMenuProvider.onCreateOptionsMenu(pMenu, menuIdOffset, mapView); + } + } + } + + if (mTilesOverlay != null && mTilesOverlay.isOptionsMenuEnabled()) { + result &= mTilesOverlay.onCreateOptionsMenu(pMenu, menuIdOffset, mapView); + } + + return result; + } + + public boolean onPrepareOptionsMenu(final Menu pMenu, final int menuIdOffset, final MapView mapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay instanceof IOverlayMenuProvider) { + final IOverlayMenuProvider overlayMenuProvider = (IOverlayMenuProvider) overlay; + if (overlayMenuProvider.isOptionsMenuEnabled()) { + overlayMenuProvider.onPrepareOptionsMenu(pMenu, menuIdOffset, mapView); + } + } + } + + if (mTilesOverlay != null && mTilesOverlay.isOptionsMenuEnabled()) { + mTilesOverlay.onPrepareOptionsMenu(pMenu, menuIdOffset, mapView); + } + + return true; + } + + public boolean onOptionsItemSelected(final MenuItem item, final int menuIdOffset, final MapView mapView) { + for (final Overlay overlay : this.overlaysReversed()) { + if (overlay instanceof IOverlayMenuProvider) { + final IOverlayMenuProvider overlayMenuProvider = (IOverlayMenuProvider) overlay; + if (overlayMenuProvider.isOptionsMenuEnabled() && + overlayMenuProvider.onOptionsItemSelected(item, menuIdOffset, mapView)) { + return true; + } + } + } + + if (mTilesOverlay != null && + mTilesOverlay.isOptionsMenuEnabled() && + mTilesOverlay.onOptionsItemSelected(item, menuIdOffset, mapView)) { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/org/osmdroid/views/overlay/PathOverlay.java b/src/main/java/org/osmdroid/views/overlay/PathOverlay.java new file mode 100644 index 000000000..e18eb83af --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/PathOverlay.java @@ -0,0 +1,261 @@ +package org.osmdroid.views.overlay; + +import java.util.ArrayList; +import java.util.List; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; + +/** + * + * @author Viesturs Zarins + * @author Martin Pearman + * + * This class draws a path line in given color. + */ +public class PathOverlay extends Overlay { + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + /** + * Stores points, converted to the map projection. + */ + private ArrayList mPoints; + + /** + * Number of points that have precomputed values. + */ + private int mPointsPrecomputed; + + /** + * Paint settings. + */ + protected Paint mPaint = new Paint(); + + private final Path mPath = new Path(); + + private final Point mTempPoint1 = new Point(); + private final Point mTempPoint2 = new Point(); + + // bounding rectangle for the current line segment. + private final Rect mLineBounds = new Rect(); + + // =========================================================== + // Constructors + // =========================================================== + + public PathOverlay(final int color, final Context ctx) { + this(color, 2.0f, new DefaultResourceProxyImpl(ctx)); + } + + public PathOverlay(final int color, final ResourceProxy resourceProxy) { + this(color, 2.0f, resourceProxy); + } + + public PathOverlay(final int color, final float width, final ResourceProxy resourceProxy) { + super(resourceProxy); + this.mPaint.setColor(color); + this.mPaint.setStrokeWidth(width); + this.mPaint.setStyle(Paint.Style.STROKE); + + this.clearPath(); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public void setColor(final int color) { + this.mPaint.setColor(color); + } + + public void setAlpha(final int a) { + this.mPaint.setAlpha(a); + } + + /** + * Draw a great circle. + * Calculate a point for every 100km along the path. + * @param startPoint start point of the great circle + * @param endPoint end point of the great circle + */ + public void addGreatCircle(final GeoPoint startPoint, final GeoPoint endPoint) { + // get the great circle path length in meters + final int greatCircleLength=startPoint.distanceTo(endPoint); + + // add one point for every 100kms of the great circle path + final int numberOfPoints=greatCircleLength/100000; + + addGreatCircle(startPoint, endPoint, numberOfPoints); + } + + /** + * Draw a great circle. + * @param startPoint start point of the great circle + * @param endPoint end point of the great circle + * @param numberOfPoints number of points to calculate along the path + */ + public void addGreatCircle(final GeoPoint startPoint, final GeoPoint endPoint, final int numberOfPoints) { + // adapted from page http://compastic.blogspot.co.uk/2011/07/how-to-draw-great-circle-on-map-in.html + // which was adapted from page http://maps.forum.nu/gm_flight_path.html + + // convert to radians + final double lat1 = startPoint.getLatitudeE6() / 1E6 * Math.PI / 180; + final double lon1 = startPoint.getLongitudeE6() / 1E6 * Math.PI / 180; + final double lat2 = endPoint.getLatitudeE6() / 1E6 * Math.PI / 180; + final double lon2 = endPoint.getLongitudeE6() / 1E6 * Math.PI / 180; + + final double d = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin((lat1 - lat2) / 2), 2) + Math.cos(lat1) * Math.cos(lat2) + * Math.pow(Math.sin((lon1 - lon2) / 2), 2))); + double bearing = Math.atan2(Math.sin(lon1 - lon2) * Math.cos(lat2), + Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2)) + / -(Math.PI / 180); + bearing = bearing < 0 ? 360 + bearing : bearing; + + for (int i = 0, j = numberOfPoints + 1; i < j; i++) { + final double f = 1.0 / numberOfPoints * i; + final double A = Math.sin((1 - f) * d) / Math.sin(d); + final double B = Math.sin(f * d) / Math.sin(d); + final double x = A * Math.cos(lat1) * Math.cos(lon1) + B * Math.cos(lat2) * Math.cos(lon2); + final double y = A * Math.cos(lat1) * Math.sin(lon1) + B * Math.cos(lat2) * Math.sin(lon2); + final double z = A * Math.sin(lat1) + B * Math.sin(lat2); + + final double latN = Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); + final double lonN = Math.atan2(y, x); + addPoint((int) (latN / (Math.PI / 180) * 1E6), (int) (lonN / (Math.PI / 180) * 1E6)); + } + } + + public Paint getPaint() { + return mPaint; + } + + public void setPaint(final Paint pPaint) { + if (pPaint == null) { + throw new IllegalArgumentException("pPaint argument cannot be null"); + } + mPaint = pPaint; + } + + public void clearPath() { + this.mPoints = new ArrayList(); + this.mPointsPrecomputed = 0; + } + + public void addPoint(final IGeoPoint aPoint) { + addPoint(aPoint.getLatitudeE6(), aPoint.getLongitudeE6()); + } + + public void addPoint(final int aLatitudeE6, final int aLongitudeE6) { + mPoints.add(new Point(aLatitudeE6, aLongitudeE6)); + } + + public void addPoints(final IGeoPoint... aPoints) { + for(final IGeoPoint point : aPoints) { + addPoint(point); + } + } + + public void addPoints(final List aPoints) { + for(final IGeoPoint point : aPoints) { + addPoint(point); + } + } + + public int getNumberOfPoints() { + return this.mPoints.size(); + } + + /** + * This method draws the line. Note - highly optimized to handle long paths, proceed with care. + * Should be fine up to 10K points. + */ + @Override + protected void draw(final Canvas canvas, final MapView mapView, final boolean shadow) { + + if (shadow) { + return; + } + + final int size = this.mPoints.size(); + if (size < 2) { + // nothing to paint + return; + } + + final Projection pj = mapView.getProjection(); + + // precompute new points to the intermediate projection. + while (this.mPointsPrecomputed < size) { + final Point pt = this.mPoints.get(this.mPointsPrecomputed); + pj.toMapPixelsProjected(pt.x, pt.y, pt); + + this.mPointsPrecomputed++; + } + + Point screenPoint0 = null; // points on screen + Point screenPoint1; + Point projectedPoint0; // points from the points list + Point projectedPoint1; + + // clipping rectangle in the intermediate projection, to avoid performing projection. + final Rect clipBounds = pj.fromPixelsToProjected(pj.getScreenRect()); + + mPath.rewind(); + projectedPoint0 = this.mPoints.get(size - 1); + mLineBounds.set(projectedPoint0.x, projectedPoint0.y, projectedPoint0.x, projectedPoint0.y); + + for (int i = size - 2; i >= 0; i--) { + // compute next points + projectedPoint1 = this.mPoints.get(i); + mLineBounds.union(projectedPoint1.x, projectedPoint1.y); + + if (!Rect.intersects(clipBounds, mLineBounds)) { + // skip this line, move to next point + projectedPoint0 = projectedPoint1; + screenPoint0 = null; + continue; + } + + // the starting point may be not calculated, because previous segment was out of clip + // bounds + if (screenPoint0 == null) { + screenPoint0 = pj.toMapPixelsTranslated(projectedPoint0, this.mTempPoint1); + mPath.moveTo(screenPoint0.x, screenPoint0.y); + } + + screenPoint1 = pj.toMapPixelsTranslated(projectedPoint1, this.mTempPoint2); + + // skip this point, too close to previous point + if (Math.abs(screenPoint1.x - screenPoint0.x) + Math.abs(screenPoint1.y - screenPoint0.y) <= 1) { + continue; + } + + mPath.lineTo(screenPoint1.x, screenPoint1.y); + + // update starting point to next position + projectedPoint0 = projectedPoint1; + screenPoint0.x = screenPoint1.x; + screenPoint0.y = screenPoint1.y; + mLineBounds.set(projectedPoint0.x, projectedPoint0.y, projectedPoint0.x, projectedPoint0.y); + } + + canvas.drawPath(mPath, this.mPaint); + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/SafeDrawOverlay.java b/src/main/java/org/osmdroid/views/overlay/SafeDrawOverlay.java new file mode 100644 index 000000000..5d969fa1c --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/SafeDrawOverlay.java @@ -0,0 +1,99 @@ +package org.osmdroid.views.overlay; + +import org.osmdroid.ResourceProxy; +import org.osmdroid.views.MapView; +import org.osmdroid.views.safecanvas.ISafeCanvas; +import org.osmdroid.views.safecanvas.SafeTranslatedCanvas; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.os.Build; + +/** + * An overlay class that uses the safe drawing canvas to draw itself and can be zoomed in to high + * levels without drawing issues. + * + * @see {@link ISafeCanvas} + */ +public abstract class SafeDrawOverlay extends Overlay { + + private static final SafeTranslatedCanvas sSafeCanvas = new SafeTranslatedCanvas(); + private static final Matrix sMatrix = new Matrix(); + private boolean mUseSafeCanvas = true; + + protected abstract void drawSafe(final ISafeCanvas c, final MapView osmv, final boolean shadow); + + public SafeDrawOverlay(Context ctx) { + super(ctx); + } + + public SafeDrawOverlay(ResourceProxy pResourceProxy) { + super(pResourceProxy); + } + + @Override + protected void draw(final Canvas c, final MapView osmv, final boolean shadow) { + + sSafeCanvas.setCanvas(c); + + if (this.isUsingSafeCanvas()) { + + // Find the screen offset + Rect screenRect = osmv.getProjection().getScreenRect(); + sSafeCanvas.xOffset = -screenRect.left; + sSafeCanvas.yOffset = -screenRect.top; + + // Save the canvas state + c.save(); + + if (osmv.getMapOrientation() != 0) { + // Un-rotate the maps so we can rotate them accurately using the safe canvas + c.rotate(-osmv.getMapOrientation(), screenRect.exactCenterX(), + screenRect.exactCenterY()); + } + + // Since the translate calls still take a float, there can be rounding errors + // Let's calculate the error, and adjust for it. + final int floatErrorX = screenRect.left - (int) (float) screenRect.left; + final int floatErrorY = screenRect.top - (int) (float) screenRect.top; + + // Translate the coordinates + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + final float scaleX = osmv.getScaleX(); + final float scaleY = osmv.getScaleY(); + c.translate(screenRect.left * scaleX, screenRect.top * scaleY); + c.translate(floatErrorX, floatErrorY); + } else { + c.getMatrix(sMatrix); + sMatrix.preTranslate(screenRect.left, screenRect.top); + sMatrix.preTranslate(floatErrorX, floatErrorY); + c.setMatrix(sMatrix); + } + + if (osmv.getMapOrientation() != 0) { + // Safely re-rotate the maps + sSafeCanvas.rotate(osmv.getMapOrientation(), (double) screenRect.exactCenterX(), + (double) screenRect.exactCenterY()); + } + + } else { + sSafeCanvas.xOffset = 0; + sSafeCanvas.yOffset = 0; + } + this.drawSafe(sSafeCanvas, osmv, shadow); + + if (this.isUsingSafeCanvas()) { + c.restore(); + } + } + + public boolean isUsingSafeCanvas() { + return mUseSafeCanvas; + } + + public void setUseSafeCanvas(boolean useSafeCanvas) { + mUseSafeCanvas = useSafeCanvas; + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/ScaleBarOverlay.java b/src/main/java/org/osmdroid/views/overlay/ScaleBarOverlay.java new file mode 100644 index 000000000..6d72efbbf --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/ScaleBarOverlay.java @@ -0,0 +1,583 @@ +package org.osmdroid.views.overlay; + +/** + * ScaleBarOverlay.java + * + * Puts a scale bar in the top-left corner of the screen, offset by a configurable + * number of pixels. The bar is scaled to 1-inch length by querying for the physical + * DPI of the screen. The size of the bar is printed between the tick marks. A + * vertical (longitude) scale can be enabled. Scale is printed in metric (kilometers, + * meters), imperial (miles, feet) and nautical (nautical miles, feet). + * + * Author: Erik Burrows, Griffin Systems LLC + * erik@griffinsystems.org + * + * Change Log: + * 2010-10-08: Inclusion to osmdroid trunk + * + * License: + * LGPL version 3 + * http://www.gnu.org/licenses/lgpl.html + * + * Usage: + * + * MapView map = new MapView(...); + * ScaleBarOverlay scaleBar = new ScaleBarOverlay(this.getBaseContext(), map); + * + * scaleBar.setImperial(); // Metric by default + * + * map.getOverlays().add(scaleBar); + * + * + * To Do List: + * 1. Allow for top, bottom, left or right placement. + * 2. Scale bar to precise displayed scale text after rounding. + * + */ + +import java.lang.reflect.Field; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.util.constants.GeoConstants; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; +import org.osmdroid.views.safecanvas.ISafeCanvas; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Picture; +import android.graphics.Rect; +import android.view.WindowManager; + +public class ScaleBarOverlay extends SafeDrawOverlay implements GeoConstants { + + // =========================================================== + // Fields + // =========================================================== + + // Defaults + + float xOffset = 10; + float yOffset = 10; + float lineWidth = 2; + final int textSize = 12; + int minZoom = 0; + + boolean imperial = false; + boolean nautical = false; + + boolean latitudeBar = true; + boolean longitudeBar = false; + + // Internal + + private final Context context; + + protected final Picture scaleBarPicture = new Picture(); + + private int lastZoomLevel = -1; + private float lastLatitude = 0; + + public float xdpi; + public float ydpi; + public int screenWidth; + public int screenHeight; + + private final ResourceProxy resourceProxy; + private Paint barPaint; + private Paint bgPaint; + private Paint textPaint; + private Projection projection; + + final private Rect mBounds = new Rect(); + final private Matrix mIdentityMatrix = new Matrix(); + + private boolean centred = false; + private boolean adjustLength = false; + private float maxLength; + + // =========================================================== + // Constructors + // =========================================================== + + public ScaleBarOverlay(final Context context) { + this(context, new DefaultResourceProxyImpl(context)); + } + + public ScaleBarOverlay(final Context context, final ResourceProxy pResourceProxy) { + super(pResourceProxy); + this.resourceProxy = pResourceProxy; + this.context = context; + + this.barPaint = new Paint(); + this.barPaint.setColor(Color.BLACK); + this.barPaint.setAntiAlias(true); + this.barPaint.setStyle(Style.FILL); + this.barPaint.setAlpha(255); + this.bgPaint = null; + + this.textPaint = new Paint(); + this.textPaint.setColor(Color.BLACK); + this.textPaint.setAntiAlias(true); + this.textPaint.setStyle(Style.FILL); + this.textPaint.setAlpha(255); + this.textPaint.setTextSize(textSize); + + this.xdpi = this.context.getResources().getDisplayMetrics().xdpi; + this.ydpi = this.context.getResources().getDisplayMetrics().ydpi; + + this.screenWidth = this.context.getResources().getDisplayMetrics().widthPixels; + this.screenHeight = this.context.getResources().getDisplayMetrics().heightPixels; + + // DPI corrections for specific models + String manufacturer = null; + try { + final Field field = android.os.Build.class.getField("MANUFACTURER"); + manufacturer = (String) field.get(null); + } catch (final Exception ignore) { + } + + if ("motorola".equals(manufacturer) && "DROIDX".equals(android.os.Build.MODEL)) { + + // If the screen is rotated, flip the x and y dpi values + WindowManager windowManager = (WindowManager) this.context + .getSystemService(Context.WINDOW_SERVICE); + if (windowManager.getDefaultDisplay().getOrientation() > 0) { + this.xdpi = (float) (this.screenWidth / 3.75); + this.ydpi = (float) (this.screenHeight / 2.1); + } else { + this.xdpi = (float) (this.screenWidth / 2.1); + this.ydpi = (float) (this.screenHeight / 3.75); + } + + } else if ("motorola".equals(manufacturer) && "Droid".equals(android.os.Build.MODEL)) { + // http://www.mail-archive.com/android-developers@googlegroups.com/msg109497.html + this.xdpi = 264; + this.ydpi = 264; + } + + // set default max length to 1 inch + maxLength = 2.54f; + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + /** + * Sets the minimum zoom level for the scale bar to be drawn. + * @param minimum zoom level + */ + public void setMinZoom(final int zoom) { + this.minZoom = zoom; + } + + /** + * Sets the scale bar screen offset for the bar. Note: if the bar is set to be drawn centered, this will be the middle of the bar, otherwise the top left corner. + * @param x x screen offset + * @param y z screen offset + */ + public void setScaleBarOffset(final float x, final float y) { + xOffset = x; + yOffset = y; + } + + /** + * Sets the bar's line width. (the default is 2) + * @param width the new line width + */ + public void setLineWidth(final float width) { + this.lineWidth = width; + } + + /** + * Sets the text size. (the default is 12) + * @param size the new text size + */ + public void setTextSize(final float size) { + this.textPaint.setTextSize(size); + } + + /** + * Sets the length to be shown in imperial units (mi/ft) + */ + public void setImperial() { + this.imperial = true; + this.nautical = false; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Sets the length to be shown in nautical units (nm/ft) + */ + public void setNautical() { + this.nautical = true; + this.imperial = false; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Sets the length to be shown in metric units (km/m) + */ + public void setMetric() { + this.nautical = false; + this.imperial = false; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Latitudinal / horizontal scale bar flag + * @param latitude + */ + public void drawLatitudeScale(final boolean latitude) { + this.latitudeBar = latitude; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Longitudinal / vertical scale bar flag + * @param longitude + */ + public void drawLongitudeScale(final boolean longitude) { + this.longitudeBar = longitude; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Flag to draw the bar centered around the set offset coordinates or to the right/bottom of the coordinates (default) + * @param centred set true to centre the bar around the given screen coordinates + */ + public void setCentred(final boolean centred) { + this.centred = centred; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Return's the paint used to draw the bar + * @return the paint used to draw the bar + */ + public Paint getBarPaint() { + return barPaint; + } + + /** + * Sets the paint for drawing the bar + * @param pBarPaint bar drawing paint + */ + public void setBarPaint(final Paint pBarPaint) { + if (pBarPaint == null) { + throw new IllegalArgumentException("pBarPaint argument cannot be null"); + } + barPaint = pBarPaint; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Returns the paint used to draw the text + * @return the paint used to draw the text + */ + public Paint getTextPaint() { + return textPaint; + } + + /** + * Sets the paint for drawing the text + * @param pTextPaint text drawing paint + */ + public void setTextPaint(final Paint pTextPaint) { + if (pTextPaint == null) { + throw new IllegalArgumentException("pTextPaint argument cannot be null"); + } + textPaint = pTextPaint; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Sets the background paint. Set to null to disable drawing of background (default) + * @param pBgPaint the paint for colouring the bar background + */ + public void setBackgroundPaint(final Paint pBgPaint) { + bgPaint = pBgPaint; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * If enabled, the bar will automatically adjust the length to reflect a round number (starting + * with 1, 2 or 5). If disabled, the bar will always be drawn in full length representing a + * fractional distance. + */ + public void setEnableAdjustLength(boolean adjustLength) { + this.adjustLength = adjustLength; + lastZoomLevel = -1; // Force redraw of scalebar + } + + /** + * Sets the maximum bar length. If adjustLength is disabled this will match exactly the length of the bar. + * If adjustLength is enabled, the bar will be shortened to reflect a round number in length. + * @param pMaxLengthInCm maximum length of the bar in the screen in cm. Default is 2.54 (=1 inch) + */ + public void setMaxLength(final float pMaxLengthInCm) { + this.maxLength = pMaxLengthInCm; + lastZoomLevel = -1; // Force redraw of scalebar + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public void drawSafe(final ISafeCanvas c, final MapView mapView, final boolean shadow) { + + if (shadow) { + return; + } + + // If map view is animating, don't update, scale will be wrong. + if (mapView.isAnimating()) { + return; + } + + final int zoomLevel = mapView.getZoomLevel(); + + if (zoomLevel >= minZoom) { + final Projection projection = mapView.getProjection(); + + if (projection == null) { + return; + } + + final IGeoPoint center = projection.fromPixels((screenWidth / 2), screenHeight / 2); + if (zoomLevel != lastZoomLevel + || (int) (center.getLatitudeE6() / 1E6) != (int) (lastLatitude / 1E6)) { + lastZoomLevel = zoomLevel; + lastLatitude = center.getLatitudeE6(); + createScaleBarPicture(mapView); + } + + mBounds.set(0, 0, scaleBarPicture.getWidth(), scaleBarPicture.getHeight()); + mBounds.offset((int) xOffset, (int) yOffset); + if (centred && latitudeBar) + mBounds.offset(-scaleBarPicture.getWidth() / 2, 0); + if (centred && longitudeBar) + mBounds.offset(0, -scaleBarPicture.getHeight() / 2); + + mBounds.set(mBounds); + c.save(); + c.setMatrix(mIdentityMatrix); + c.getWrappedCanvas().drawPicture(scaleBarPicture, mBounds); + c.restore(); + } + } + + // =========================================================== + // Methods + // =========================================================== + + public void disableScaleBar() { + setEnabled(false); + } + + public void enableScaleBar() { + setEnabled(true); + } + + private void createScaleBarPicture(final MapView mapView) { + // We want the scale bar to be as long as the closest round-number miles/kilometers + // to 1-inch at the latitude at the current center of the screen. + + projection = mapView.getProjection(); + + if (projection == null) { + return; + } + + // calculate dots per centimeter + int xdpcm = (int) ((float) xdpi / 2.54); + int ydpcm = (int) ((float) ydpi / 2.54); + + // get length in pixel + int xLen = (int) (maxLength * xdpcm); + int yLen = (int) (maxLength * ydpcm); + + // Two points, xLen apart, at scale bar screen location + IGeoPoint p1 = projection.fromPixels((screenWidth / 2) - (xLen / 2), yOffset); + IGeoPoint p2 = projection.fromPixels((screenWidth / 2) + (xLen / 2), yOffset); + + // get distance in meters between points + final int xMeters = ((GeoPoint) p1).distanceTo(p2); + // get adjusted distance, shortened to the next lower number starting with 1, 2 or 5 + final double xMetersAdjusted = this.adjustLength ? adjustScaleBarLength(xMeters) : xMeters; + // get adjusted length in pixels + final int xBarLengthPixels = (int) (xLen * xMetersAdjusted / xMeters); + + // Two points, yLen apart, at scale bar screen location + p1 = projection.fromPixels(screenWidth / 2, (screenHeight / 2) - (yLen / 2)); + p2 = projection.fromPixels(screenWidth / 2, (screenHeight / 2) + (yLen / 2)); + + // get distance in meters between points + final int yMeters = ((GeoPoint) p1).distanceTo(p2); + // get adjusted distance, shortened to the next lower number starting with 1, 2 or 5 + final double yMetersAdjusted = this.adjustLength ? adjustScaleBarLength(yMeters) : yMeters; + // get adjusted length in pixels + final int yBarLengthPixels = (int) (yLen * yMetersAdjusted / yMeters); + + final Canvas canvas = scaleBarPicture.beginRecording(xBarLengthPixels, yBarLengthPixels); + + // create text + final String xMsg = scaleBarLengthText((int) xMetersAdjusted, imperial, nautical); + final Rect xTextRect = new Rect(); + textPaint.getTextBounds(xMsg, 0, xMsg.length(), xTextRect); + final int xTextSpacing = (int) (xTextRect.height() / 5.0); + + final String yMsg = scaleBarLengthText((int) yMetersAdjusted, imperial, nautical); + final Rect yTextRect = new Rect(); + textPaint.getTextBounds(yMsg, 0, yMsg.length(), yTextRect); + final int yTextSpacing = (int) (yTextRect.height() / 5.0); + + // paint background + if (bgPaint != null) { + canvas.drawRect(0, 0, yTextRect.height() + 2 * lineWidth + yTextSpacing, + xTextRect.height() + 2 * lineWidth + xTextSpacing, bgPaint); + if (latitudeBar) + canvas.drawRect(yTextRect.height() + 2 * lineWidth + yTextSpacing, 0, + xBarLengthPixels + lineWidth, xTextRect.height() + 2 * lineWidth + + xTextSpacing, bgPaint); + if (longitudeBar) + canvas.drawRect(0, xTextRect.height() + 2 * lineWidth + xTextSpacing, + yTextRect.height() + 2 * lineWidth + yTextSpacing, yBarLengthPixels + + lineWidth, bgPaint); + } + + // draw latitude bar + if (latitudeBar) { + canvas.drawRect(0, 0, xBarLengthPixels, lineWidth, barPaint); + canvas.drawRect(xBarLengthPixels, 0, xBarLengthPixels + lineWidth, xTextRect.height() + + lineWidth + xTextSpacing, barPaint); + + if (!longitudeBar) { + canvas.drawRect(0, 0, lineWidth, xTextRect.height() + lineWidth + xTextSpacing, + barPaint); + } + + canvas.drawText(xMsg, xBarLengthPixels / 2 - xTextRect.width() / 2, xTextRect.height() + + lineWidth + xTextSpacing, textPaint); + } + + // draw longitude bar + if (longitudeBar) { + canvas.drawRect(0, 0, lineWidth, yBarLengthPixels, barPaint); + canvas.drawRect(0, yBarLengthPixels, yTextRect.height() + lineWidth + yTextSpacing, + yBarLengthPixels + lineWidth, barPaint); + + if (!latitudeBar) { + canvas.drawRect(0, 0, yTextRect.height() + lineWidth + yTextSpacing, lineWidth, + barPaint); + } + + final float x = yTextRect.height() + lineWidth + yTextSpacing; + final float y = yBarLengthPixels / 2 + yTextRect.width() / 2; + + canvas.rotate(-90, x, y); + canvas.drawText(yMsg, x, y, textPaint); + } + + scaleBarPicture.endRecording(); + } + + /** + * Returns a reduced length that starts with 1, 2 or 5 and trailing zeros. If set to nautical or imperial the + * input will be transformed before and after the reduction so that the result holds in that respective unit. + * @param length length to round + * @return reduced, rounded (in m, nm or mi depending on setting) result + */ + private double adjustScaleBarLength(double length) { + long pow = 0; + boolean feet = false; + if (this.imperial) { + if (length >= GeoConstants.METERS_PER_STATUTE_MILE / 5) + length = length / GeoConstants.METERS_PER_STATUTE_MILE; + else { + length = length * GeoConstants.FEET_PER_METER; + feet = true; + } + } else if (this.nautical) { + if (length >= GeoConstants.METERS_PER_NAUTICAL_MILE / 5) + length = length / GeoConstants.METERS_PER_NAUTICAL_MILE; + else { + length = length * GeoConstants.FEET_PER_METER; + feet = true; + } + } + + while (length >= 10) { + pow++; + length /= 10; + } + while (length < 1 && length > 0) { + pow--; + length *= 10; + } + + if (length < 2) { + length = 1; + } else if (length < 5) { + length = 2; + } else { + length = 5; + } + if (feet) + length = length / GeoConstants.FEET_PER_METER; + else if (this.imperial) + length = length * GeoConstants.METERS_PER_STATUTE_MILE; + else if (this.nautical) + length = length * GeoConstants.METERS_PER_NAUTICAL_MILE; + length *= Math.pow(10, pow); + return length; + } + + protected String scaleBarLengthText(final int meters, final boolean imperial, + final boolean nautical) { + if (this.imperial) { + if (meters >= METERS_PER_STATUTE_MILE * 5) { + return resourceProxy.getString(ResourceProxy.string.format_distance_miles, + (int) (meters / METERS_PER_STATUTE_MILE)); + + } else if (meters >= METERS_PER_STATUTE_MILE / 5) { + return resourceProxy.getString(ResourceProxy.string.format_distance_miles, + ((int) (meters / (METERS_PER_STATUTE_MILE / 10.0))) / 10.0); + } else { + return resourceProxy.getString(ResourceProxy.string.format_distance_feet, + (int) (meters * FEET_PER_METER)); + } + } else if (this.nautical) { + if (meters >= METERS_PER_NAUTICAL_MILE * 5) { + return resourceProxy.getString(ResourceProxy.string.format_distance_nautical_miles, + ((int) (meters / METERS_PER_NAUTICAL_MILE))); + } else if (meters >= METERS_PER_NAUTICAL_MILE / 5) { + return resourceProxy.getString(ResourceProxy.string.format_distance_nautical_miles, + (((int) (meters / (METERS_PER_NAUTICAL_MILE / 10.0))) / 10.0)); + } else { + return resourceProxy.getString(ResourceProxy.string.format_distance_feet, + ((int) (meters * FEET_PER_METER))); + } + } else { + if (meters >= 1000 * 5) { + return resourceProxy.getString(ResourceProxy.string.format_distance_kilometers, + (meters / 1000)); + } else if (meters >= 1000 / 5) { + return resourceProxy.getString(ResourceProxy.string.format_distance_kilometers, + (int) (meters / 100.0) / 10.0); + } else { + return resourceProxy.getString(ResourceProxy.string.format_distance_meters, meters); + } + } + } + +} diff --git a/src/main/java/org/osmdroid/views/overlay/SimpleLocationOverlay.java b/src/main/java/org/osmdroid/views/overlay/SimpleLocationOverlay.java new file mode 100644 index 000000000..8a16cafc3 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/SimpleLocationOverlay.java @@ -0,0 +1,87 @@ +// Created by plusminus on 22:01:11 - 29.09.2008 +package org.osmdroid.views.overlay; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; + +/** + * + * @author Nicolas Gramlich + * + */ +public class SimpleLocationOverlay extends Overlay { + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected final Paint mPaint = new Paint(); + + protected final Bitmap PERSON_ICON; + /** Coordinates the feet of the person are located. */ + protected final android.graphics.Point PERSON_HOTSPOT = new android.graphics.Point(24, 39); + + protected GeoPoint mLocation; + private final Point screenCoords = new Point(); + + // =========================================================== + // Constructors + // =========================================================== + + public SimpleLocationOverlay(final Context ctx) { + this(ctx, new DefaultResourceProxyImpl(ctx)); + } + + public SimpleLocationOverlay(final Context ctx, + final ResourceProxy pResourceProxy) { + super(pResourceProxy); + this.PERSON_ICON = mResourceProxy.getBitmap(ResourceProxy.bitmap.person); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public void setLocation(final GeoPoint mp) { + this.mLocation = mp; + } + + public GeoPoint getMyLocation() { + return this.mLocation; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + public void draw(final Canvas c, final MapView osmv, final boolean shadow) { + if (!shadow && this.mLocation != null) { + final Projection pj = osmv.getProjection(); + pj.toMapPixels(this.mLocation, screenCoords); + + c.drawBitmap(PERSON_ICON, screenCoords.x - PERSON_HOTSPOT.x, screenCoords.y + - PERSON_HOTSPOT.y, this.mPaint); + } + } + + // =========================================================== + // Methods + // =========================================================== + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/views/overlay/TilesOverlay.java b/src/main/java/org/osmdroid/views/overlay/TilesOverlay.java new file mode 100644 index 000000000..8b9c414ff --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/TilesOverlay.java @@ -0,0 +1,374 @@ +package org.osmdroid.views.overlay; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.tileprovider.MapTile; +import org.osmdroid.tileprovider.MapTileProviderBase; +import org.osmdroid.tileprovider.ReusableBitmapDrawable; +import org.osmdroid.tileprovider.tilesource.ITileSource; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.util.TileLooper; +import org.osmdroid.util.TileSystem; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; +import org.osmdroid.views.safecanvas.ISafeCanvas; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SubMenu; + +/** + * These objects are the principle consumer of map tiles. + * + * see {@link MapTile} for an overview of how tiles are acquired by this overlay. + * + */ + +public class TilesOverlay extends SafeDrawOverlay implements IOverlayMenuProvider { + + private static final Logger logger = LoggerFactory.getLogger(TilesOverlay.class); + + public static final int MENU_MAP_MODE = getSafeMenuId(); + public static final int MENU_TILE_SOURCE_STARTING_ID = getSafeMenuIdSequence(TileSourceFactory + .getTileSources().size()); + public static final int MENU_OFFLINE = getSafeMenuId(); + + /** Current tile source */ + protected final MapTileProviderBase mTileProvider; + + /* to avoid allocations during draw */ + protected final Paint mDebugPaint = new Paint(); + private final Rect mTileRect = new Rect(); + private final Rect mViewPort = new Rect(); + + private boolean mOptionsMenuEnabled = true; + + private int mWorldSize_2; + + /** A drawable loading tile **/ + private BitmapDrawable mLoadingTile = null; + private int mLoadingBackgroundColor = Color.rgb(216, 208, 208); + private int mLoadingLineColor = Color.rgb(200, 192, 192); + + /** For overshooting the tile cache **/ + private int mOvershootTileCache = 0; + + public TilesOverlay(final MapTileProviderBase aTileProvider, final Context aContext) { + this(aTileProvider, new DefaultResourceProxyImpl(aContext)); + } + + public TilesOverlay(final MapTileProviderBase aTileProvider, final ResourceProxy pResourceProxy) { + super(pResourceProxy); + if (aTileProvider == null) { + throw new IllegalArgumentException( + "You must pass a valid tile provider to the tiles overlay."); + } + this.mTileProvider = aTileProvider; + } + + @Override + public void onDetach(final MapView pMapView) { + this.mTileProvider.detach(); + } + + public int getMinimumZoomLevel() { + return mTileProvider.getMinimumZoomLevel(); + } + + public int getMaximumZoomLevel() { + return mTileProvider.getMaximumZoomLevel(); + } + + /** + * Whether to use the network connection if it's available. + */ + public boolean useDataConnection() { + return mTileProvider.useDataConnection(); + } + + /** + * Set whether to use the network connection if it's available. + * + * @param aMode + * if true use the network connection if it's available. if false don't use the + * network connection even if it's available. + */ + public void setUseDataConnection(final boolean aMode) { + mTileProvider.setUseDataConnection(aMode); + } + + @Override + protected void drawSafe(final ISafeCanvas c, final MapView osmv, final boolean shadow) { + + if (DEBUGMODE) { + logger.trace("onDraw(" + shadow + ")"); + } + + if (shadow) { + return; + } + + // Calculate the half-world size + final Projection pj = osmv.getProjection(); + final int zoomLevel = pj.getZoomLevel(); + mWorldSize_2 = TileSystem.MapSize(zoomLevel) >> 1; + + // Get the area we are drawing to + mViewPort.set(pj.getScreenRect()); + + // Translate the Canvas coordinates into Mercator coordinates + mViewPort.offset(mWorldSize_2, mWorldSize_2); + + // Draw the tiles! + drawTiles(c.getSafeCanvas(), pj.getZoomLevel(), TileSystem.getTileSize(), mViewPort); + } + + /** + * This is meant to be a "pure" tile drawing function that doesn't take into account + * osmdroid-specific characteristics (like osmdroid's canvas's having 0,0 as the center rather + * than the upper-left corner). Once the tile is ready to be drawn, it is passed to + * onTileReadyToDraw where custom manipulations can be made before drawing the tile. + */ + public void drawTiles(final Canvas c, final int zoomLevel, final int tileSizePx, + final Rect viewPort) { + + mTileLooper.loop(c, zoomLevel, tileSizePx, viewPort); + + // draw a cross at center in debug mode + if (DEBUGMODE) { + // final GeoPoint center = osmv.getMapCenter(); + final Point centerPoint = new Point(viewPort.centerX() - mWorldSize_2, + viewPort.centerY() - mWorldSize_2); + c.drawLine(centerPoint.x, centerPoint.y - 9, centerPoint.x, centerPoint.y + 9, mDebugPaint); + c.drawLine(centerPoint.x - 9, centerPoint.y, centerPoint.x + 9, centerPoint.y, mDebugPaint); + } + + } + + private final TileLooper mTileLooper = new TileLooper() { + @Override + public void initialiseLoop(final int pZoomLevel, final int pTileSizePx) { + // make sure the cache is big enough for all the tiles + final int numNeeded = (mLowerRight.y - mUpperLeft.y + 1) * (mLowerRight.x - mUpperLeft.x + 1); + mTileProvider.ensureCapacity(numNeeded + mOvershootTileCache); + } + @Override + public void handleTile(final Canvas pCanvas, final int pTileSizePx, final MapTile pTile, final int pX, final int pY) { + Drawable currentMapTile = mTileProvider.getMapTile(pTile); + boolean isReusable = currentMapTile instanceof ReusableBitmapDrawable; + if (currentMapTile == null) { + currentMapTile = getLoadingTile(); + } + + if (currentMapTile != null) { + mTileRect.set(pX * pTileSizePx, pY * pTileSizePx, pX * pTileSizePx + pTileSizePx, + pY * pTileSizePx + pTileSizePx); + if (isReusable) + ((ReusableBitmapDrawable) currentMapTile).beginUsingDrawable(); + try { + if (isReusable && !((ReusableBitmapDrawable) currentMapTile).isBitmapValid()) { + currentMapTile = getLoadingTile(); + isReusable = false; + } + onTileReadyToDraw(pCanvas, currentMapTile, mTileRect); + } finally { + if (isReusable) + ((ReusableBitmapDrawable) currentMapTile).finishUsingDrawable(); + } + } + + if (DEBUGMODE) { + mTileRect.set(pX * pTileSizePx, pY * pTileSizePx, pX * pTileSizePx + pTileSizePx, pY + * pTileSizePx + pTileSizePx); + mTileRect.offset(-mWorldSize_2, -mWorldSize_2); + pCanvas.drawText(pTile.toString(), mTileRect.left + 1, + mTileRect.top + mDebugPaint.getTextSize(), mDebugPaint); + pCanvas.drawLine(mTileRect.left, mTileRect.top, mTileRect.right, mTileRect.top, + mDebugPaint); + pCanvas.drawLine(mTileRect.left, mTileRect.top, mTileRect.left, mTileRect.bottom, + mDebugPaint); + } + } + @Override + public void finaliseLoop() { + } + }; + + protected void onTileReadyToDraw(final Canvas c, final Drawable currentMapTile, + final Rect tileRect) { + tileRect.offset(-mWorldSize_2, -mWorldSize_2); + currentMapTile.setBounds(tileRect); + currentMapTile.draw(c); + } + + @Override + public void setOptionsMenuEnabled(final boolean pOptionsMenuEnabled) { + this.mOptionsMenuEnabled = pOptionsMenuEnabled; + } + + @Override + public boolean isOptionsMenuEnabled() { + return this.mOptionsMenuEnabled; + } + + @Override + public boolean onCreateOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView) { + final SubMenu mapMenu = pMenu.addSubMenu(0, MENU_MAP_MODE + pMenuIdOffset, Menu.NONE, + mResourceProxy.getString(ResourceProxy.string.map_mode)).setIcon( + mResourceProxy.getDrawable(ResourceProxy.bitmap.ic_menu_mapmode)); + + for (int a = 0; a < TileSourceFactory.getTileSources().size(); a++) { + final ITileSource tileSource = TileSourceFactory.getTileSources().get(a); + mapMenu.add(MENU_MAP_MODE + pMenuIdOffset, MENU_TILE_SOURCE_STARTING_ID + a + + pMenuIdOffset, Menu.NONE, tileSource.localizedName(mResourceProxy)); + } + mapMenu.setGroupCheckable(MENU_MAP_MODE + pMenuIdOffset, true, true); + + final String title = pMapView.getResourceProxy().getString( + pMapView.useDataConnection() ? ResourceProxy.string.offline_mode + : ResourceProxy.string.online_mode); + final Drawable icon = pMapView.getResourceProxy().getDrawable( + ResourceProxy.bitmap.ic_menu_offline); + pMenu.add(0, MENU_OFFLINE + pMenuIdOffset, Menu.NONE, title).setIcon(icon); + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView) { + final int index = TileSourceFactory.getTileSources().indexOf( + pMapView.getTileProvider().getTileSource()); + if (index >= 0) { + pMenu.findItem(MENU_TILE_SOURCE_STARTING_ID + index + pMenuIdOffset).setChecked(true); + } + + pMenu.findItem(MENU_OFFLINE + pMenuIdOffset).setTitle( + pMapView.getResourceProxy().getString( + pMapView.useDataConnection() ? ResourceProxy.string.offline_mode + : ResourceProxy.string.online_mode)); + + return true; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem pItem, final int pMenuIdOffset, + final MapView pMapView) { + + final int menuId = pItem.getItemId() - pMenuIdOffset; + if ((menuId >= MENU_TILE_SOURCE_STARTING_ID) + && (menuId < MENU_TILE_SOURCE_STARTING_ID + + TileSourceFactory.getTileSources().size())) { + pMapView.setTileSource(TileSourceFactory.getTileSources().get( + menuId - MENU_TILE_SOURCE_STARTING_ID)); + return true; + } else if (menuId == MENU_OFFLINE) { + final boolean useDataConnection = !pMapView.useDataConnection(); + pMapView.setUseDataConnection(useDataConnection); + return true; + } else { + return false; + } + } + + public int getLoadingBackgroundColor() { + return mLoadingBackgroundColor; + } + + /** + * Set the color to use to draw the background while we're waiting for the tile to load. + * + * @param pLoadingBackgroundColor + * the color to use. If the value is {@link Color#TRANSPARENT} then there will be no + * loading tile. + */ + public void setLoadingBackgroundColor(final int pLoadingBackgroundColor) { + if (mLoadingBackgroundColor != pLoadingBackgroundColor) { + mLoadingBackgroundColor = pLoadingBackgroundColor; + clearLoadingTile(); + } + } + + public int getLoadingLineColor() { + return mLoadingLineColor; + } + + public void setLoadingLineColor(final int pLoadingLineColor) { + if (mLoadingLineColor != pLoadingLineColor) { + mLoadingLineColor = pLoadingLineColor; + clearLoadingTile(); + } + } + + private Drawable getLoadingTile() { + if (mLoadingTile == null && mLoadingBackgroundColor != Color.TRANSPARENT) { + try { + final int tileSize = mTileProvider.getTileSource() != null ? mTileProvider + .getTileSource().getTileSizePixels() : 256; + final Bitmap bitmap = Bitmap.createBitmap(tileSize, tileSize, + Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + final Paint paint = new Paint(); + canvas.drawColor(mLoadingBackgroundColor); + paint.setColor(mLoadingLineColor); + paint.setStrokeWidth(0); + final int lineSize = tileSize / 16; + for (int a = 0; a < tileSize; a += lineSize) { + canvas.drawLine(0, a, tileSize, a, paint); + canvas.drawLine(a, 0, a, tileSize, paint); + } + mLoadingTile = new BitmapDrawable(bitmap); + } catch (final OutOfMemoryError e) { + logger.error("OutOfMemoryError getting loading tile"); + System.gc(); + } + } + return mLoadingTile; + } + + private void clearLoadingTile() { + final BitmapDrawable bitmapDrawable = mLoadingTile; + mLoadingTile = null; + // Only recycle if we are running on a project less than 2.3.3 Gingerbread. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { + if (bitmapDrawable != null) { + bitmapDrawable.getBitmap().recycle(); + } + } + } + + /** + * Set this to overshoot the tile cache. By default the TilesOverlay only creates a cache large + * enough to hold the minimum number of tiles necessary to draw to the screen. Setting this + * value will allow you to overshoot the tile cache and allow more tiles to be cached. This + * increases the memory usage, but increases drawing performance. + * + * @param overshootTileCache + * the number of tiles to overshoot the tile cache by + */ + public void setOvershootTileCache(int overshootTileCache) { + mOvershootTileCache = overshootTileCache; + } + + /** + * Get the tile cache overshoot value. + * + * @return the number of tiles to overshoot tile cache + */ + public int getOvershootTileCache() { + return mOvershootTileCache; + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/compass/CompassOverlay.java b/src/main/java/org/osmdroid/views/overlay/compass/CompassOverlay.java new file mode 100644 index 000000000..900bca900 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/compass/CompassOverlay.java @@ -0,0 +1,448 @@ +// Created by plusminus on 22:01:11 - 29.09.2008 +package org.osmdroid.views.overlay.compass; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.IOverlayMenuProvider; +import org.osmdroid.views.overlay.SafeDrawOverlay; +import org.osmdroid.views.safecanvas.ISafeCanvas; +import org.osmdroid.views.safecanvas.SafePaint; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.FloatMath; +import android.view.Display; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Surface; +import android.view.WindowManager; + +/** + * + * @author Marc Kurtz + * @author Manuel Stahl + * + */ +public class CompassOverlay extends SafeDrawOverlay implements IOverlayMenuProvider, IOrientationConsumer +{ + protected final MapView mMapView; + private final Display mDisplay; + + public IOrientationProvider mOrientationProvider; + + protected final SafePaint mPaint = new SafePaint(); + protected final Picture mCompassFrame = new Picture(); + protected final Picture mCompassRose = new Picture(); + private final Matrix mCompassMatrix = new Matrix(); + private boolean mIsCompassEnabled; + + /** + * The bearing, in degrees east of north, or NaN if none has been set. + */ + private float mAzimuth = Float.NaN; + + private float mCompassCenterX = 35.0f; + private float mCompassCenterY = 35.0f; + private final float mCompassRadius = 20.0f; + + protected final float mCompassFrameCenterX; + protected final float mCompassFrameCenterY; + protected final float mCompassRoseCenterX; + protected final float mCompassRoseCenterY; + + public static final int MENU_COMPASS = getSafeMenuId(); + + private boolean mOptionsMenuEnabled = true; + + // =========================================================== + // Constructors + // =========================================================== + + public CompassOverlay(Context context, MapView mapView) { + this(context, new InternalCompassOrientationProvider(context), mapView); + } + + public CompassOverlay(Context context, IOrientationProvider orientationProvider, MapView mapView) + { + this(context, orientationProvider, mapView, new DefaultResourceProxyImpl(context)); + } + + public CompassOverlay(Context context, IOrientationProvider orientationProvider, + MapView mapView, ResourceProxy pResourceProxy) + { + super(pResourceProxy); + + mMapView = mapView; + final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mDisplay = windowManager.getDefaultDisplay(); + + createCompassFramePicture(); + createCompassRosePicture(); + + mCompassFrameCenterX = mCompassFrame.getWidth() / 2 - 0.5f; + mCompassFrameCenterY = mCompassFrame.getHeight() / 2 - 0.5f; + mCompassRoseCenterX = mCompassRose.getWidth() / 2 - 0.5f; + mCompassRoseCenterY = mCompassRose.getHeight() / 2 - 0.5f; + + setOrientationProvider(orientationProvider); + } + + @Override + public void onDetach(MapView mapView) { + this.disableCompass(); + super.onDetach(mapView); + } + + private void invalidateCompass() + { + Rect screenRect = mMapView.getProjection().getScreenRect(); + final int frameLeft = screenRect.left + (mMapView.getWidth() / 2) + + (int) FloatMath.ceil((mCompassCenterX - mCompassFrameCenterX) * mScale); + final int frameTop = screenRect.top + (mMapView.getHeight() / 2) + + (int) FloatMath.ceil((mCompassCenterY - mCompassFrameCenterY) * mScale); + final int frameRight = screenRect.left + (mMapView.getWidth() / 2) + + (int) FloatMath.ceil((mCompassCenterX + mCompassFrameCenterX) * mScale); + final int frameBottom = screenRect.top + (mMapView.getHeight() / 2) + + (int) FloatMath.ceil((mCompassCenterY + mCompassFrameCenterY) * mScale); + + // Expand by 2 to cover stroke width + mMapView.postInvalidate(frameLeft - 2, frameTop - 2, frameRight + 2, frameBottom + 2); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public void setCompassCenter(final float x, final float y) + { + mCompassCenterX = x; + mCompassCenterY = y; + } + + public IOrientationProvider getOrientationProvider() + { + return mOrientationProvider; + } + + protected void setOrientationProvider(IOrientationProvider orientationProvider) + { + if (orientationProvider == null) + throw new RuntimeException( + "You must pass an IOrientationProvider to setOrientationProvider()"); + + if (mOrientationProvider != null) + mOrientationProvider.stopOrientationProvider(); + + mOrientationProvider = orientationProvider; + } + + protected void drawCompass(final ISafeCanvas canvas, final float bearing, final Rect screenRect) + { + final float centerX = mCompassCenterX * mScale; + final float centerY = mCompassCenterY * mScale + (canvas.getHeight() - mMapView.getHeight()); + + mCompassMatrix.setTranslate(-mCompassFrameCenterX, -mCompassFrameCenterY); + mCompassMatrix.postTranslate(centerX, centerY); + + canvas.save(); + canvas.setMatrix(mCompassMatrix); + canvas.drawPicture(mCompassFrame); + + mCompassMatrix.setRotate(-bearing, mCompassRoseCenterX, mCompassRoseCenterY); + mCompassMatrix.postTranslate(-mCompassRoseCenterX, -mCompassRoseCenterY); + mCompassMatrix.postTranslate(centerX, centerY); + + canvas.setMatrix(mCompassMatrix); + canvas.drawPicture(mCompassRose); + canvas.restore(); + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + protected void drawSafe(ISafeCanvas canvas, MapView mapView, boolean shadow) + { + if (shadow) { + return; + } + + if (isCompassEnabled() && !Float.isNaN(mAzimuth)) { + drawCompass(canvas, mAzimuth + getDisplayOrientation(), mapView.getProjection().getScreenRect()); + } + } + + // =========================================================== + // Menu handling methods + // =========================================================== + + @Override + public void setOptionsMenuEnabled(final boolean pOptionsMenuEnabled) + { + this.mOptionsMenuEnabled = pOptionsMenuEnabled; + } + + @Override + public boolean isOptionsMenuEnabled() + { + return this.mOptionsMenuEnabled; + } + + @Override + public boolean onCreateOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView) + { + pMenu.add(0, MENU_COMPASS + pMenuIdOffset, Menu.NONE, + mResourceProxy.getString(ResourceProxy.string.compass)) + .setIcon(mResourceProxy.getDrawable(ResourceProxy.bitmap.ic_menu_compass)) + .setCheckable(true); + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(final Menu pMenu, final int pMenuIdOffset, final MapView pMapView) + { + pMenu.findItem(MENU_COMPASS + pMenuIdOffset).setChecked(this.isCompassEnabled()); + return false; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem pItem, final int pMenuIdOffset, final MapView pMapView) + { + final int menuId = pItem.getItemId() - pMenuIdOffset; + if (menuId == MENU_COMPASS) { + if (this.isCompassEnabled()) { + this.disableCompass(); + } else { + this.enableCompass(); + } + return true; + } else { + return false; + } + } + + // =========================================================== + // Methods + // =========================================================== + + @Override + public void onOrientationChanged(float orientation, IOrientationProvider source) + { + mAzimuth = orientation; + this.invalidateCompass(); + } + + public boolean enableCompass(IOrientationProvider orientationProvider) + { + this.setOrientationProvider(orientationProvider); + mIsCompassEnabled = false; + return enableCompass(); + } + + /** + * Enable receiving orientation updates from the provided IOrientationProvider and show a compass on the + * map. You will likely want to call enableCompass() from your Activity's Activity.onResume() method, to + * enable the features of this overlay. Remember to call the corresponding disableCompass() in your + * Activity's Activity.onPause() method to turn off updates when in the background. + */ + public boolean enableCompass() + { + boolean result = true; + + if (mIsCompassEnabled) + mOrientationProvider.stopOrientationProvider(); + + result = mOrientationProvider.startOrientationProvider(this); + mIsCompassEnabled = result; + + // Update the screen to see changes take effect + if (mMapView != null) { + this.invalidateCompass(); + } + + return result; + } + + /** + * Disable orientation updates + */ + public void disableCompass() + { + mIsCompassEnabled = false; + + if (mOrientationProvider != null) { + mOrientationProvider.stopOrientationProvider(); + } + + // Reset values + mAzimuth = Float.NaN; + + // Update the screen to see changes take effect + if (mMapView != null) { + this.invalidateCompass(); + } + } + + /** + * If enabled, the map is receiving orientation updates and drawing your location on the map. + * + * @return true if enabled, false otherwise + */ + public boolean isCompassEnabled() + { + return mIsCompassEnabled; + } + + public float getOrientation() + { + return mAzimuth; + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== + + private Point calculatePointOnCircle(final float centerX, final float centerY, final float radius, + final float degrees) + { + // for trigonometry, 0 is pointing east, so subtract 90 + // compass degrees are the wrong way round + final double dblRadians = Math.toRadians(-degrees + 90); + + final int intX = (int) (radius * Math.cos(dblRadians)); + final int intY = (int) (radius * Math.sin(dblRadians)); + + return new Point((int) centerX + intX, (int) centerY - intY); + } + + private void drawTriangle(final Canvas canvas, final float x, final float y, final float radius, + final float degrees, final Paint paint) + { + canvas.save(); + final Point point = this.calculatePointOnCircle(x, y, radius, degrees); + canvas.rotate(degrees, point.x, point.y); + final Path p = new Path(); + p.moveTo(point.x - 2 * mScale, point.y); + p.lineTo(point.x + 2 * mScale, point.y); + p.lineTo(point.x, point.y - 5 * mScale); + p.close(); + canvas.drawPath(p, paint); + canvas.restore(); + } + + private int getDisplayOrientation() + { + switch (mDisplay.getOrientation()) { + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + default: + return 0; + } + } + + private void createCompassFramePicture() + { + // The inside of the compass is white and transparent + final Paint innerPaint = new Paint(); + innerPaint.setColor(Color.WHITE); + innerPaint.setAntiAlias(true); + innerPaint.setStyle(Style.FILL); + innerPaint.setAlpha(200); + + // The outer part (circle and little triangles) is gray and transparent + final Paint outerPaint = new Paint(); + outerPaint.setColor(Color.GRAY); + outerPaint.setAntiAlias(true); + outerPaint.setStyle(Style.STROKE); + outerPaint.setStrokeWidth(2.0f); + outerPaint.setAlpha(200); + + final int picBorderWidthAndHeight = (int) ((mCompassRadius + 5) * 2); + final int center = picBorderWidthAndHeight / 2; + + final Canvas canvas = mCompassFrame.beginRecording(picBorderWidthAndHeight, picBorderWidthAndHeight); + + // draw compass inner circle and border + canvas.drawCircle(center, center, mCompassRadius * mScale, innerPaint); + canvas.drawCircle(center, center, mCompassRadius * mScale, outerPaint); + + // Draw little triangles north, south, west and east (don't move) + // to make those move use "-bearing + 0" etc. (Note: that would mean to draw the triangles + // in the onDraw() method) + drawTriangle(canvas, center, center, mCompassRadius * mScale, 0, outerPaint); + drawTriangle(canvas, center, center, mCompassRadius * mScale, 90, outerPaint); + drawTriangle(canvas, center, center, mCompassRadius * mScale, 180, outerPaint); + drawTriangle(canvas, center, center, mCompassRadius * mScale, 270, outerPaint); + + mCompassFrame.endRecording(); + } + + private void createCompassRosePicture() + { + // Paint design of north triangle (it's common to paint north in red color) + final Paint northPaint = new Paint(); + northPaint.setColor(0xFFA00000); + northPaint.setAntiAlias(true); + northPaint.setStyle(Style.FILL); + northPaint.setAlpha(220); + + // Paint design of south triangle (black) + final Paint southPaint = new Paint(); + southPaint.setColor(Color.BLACK); + southPaint.setAntiAlias(true); + southPaint.setStyle(Style.FILL); + southPaint.setAlpha(220); + + // Create a little white dot in the middle of the compass rose + final Paint centerPaint = new Paint(); + centerPaint.setColor(Color.WHITE); + centerPaint.setAntiAlias(true); + centerPaint.setStyle(Style.FILL); + centerPaint.setAlpha(220); + + // final int picBorderWidthAndHeight = (int) ((mCompassRadius + 5) * 2 * mScale); + final int picBorderWidthAndHeight = (int) ((mCompassRadius + 5) * 2); + final int center = picBorderWidthAndHeight / 2; + + final Canvas canvas = mCompassRose.beginRecording(picBorderWidthAndHeight, picBorderWidthAndHeight); + + // Blue triangle pointing north + final Path pathNorth = new Path(); + pathNorth.moveTo(center, center - (mCompassRadius - 3) * mScale); + pathNorth.lineTo(center + 4 * mScale, center); + pathNorth.lineTo(center - 4 * mScale, center); + pathNorth.lineTo(center, center - (mCompassRadius - 3) * mScale); + pathNorth.close(); + canvas.drawPath(pathNorth, northPaint); + + // Red triangle pointing south + final Path pathSouth = new Path(); + pathSouth.moveTo(center, center + (mCompassRadius - 3) * mScale); + pathSouth.lineTo(center + 4 * mScale, center); + pathSouth.lineTo(center - 4 * mScale, center); + pathSouth.lineTo(center, center + (mCompassRadius - 3) * mScale); + pathSouth.close(); + canvas.drawPath(pathSouth, southPaint); + + // Draw a little white dot in the middle + canvas.drawCircle(center, center, 2, centerPaint); + + mCompassRose.endRecording(); + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/compass/IOrientationConsumer.java b/src/main/java/org/osmdroid/views/overlay/compass/IOrientationConsumer.java new file mode 100644 index 000000000..c65448b0f --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/compass/IOrientationConsumer.java @@ -0,0 +1,7 @@ +package org.osmdroid.views.overlay.compass; + + +public interface IOrientationConsumer +{ + void onOrientationChanged(float orientation, IOrientationProvider source); +} diff --git a/src/main/java/org/osmdroid/views/overlay/compass/IOrientationProvider.java b/src/main/java/org/osmdroid/views/overlay/compass/IOrientationProvider.java new file mode 100644 index 000000000..9e9dbcdcc --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/compass/IOrientationProvider.java @@ -0,0 +1,11 @@ +package org.osmdroid.views.overlay.compass; + + +public interface IOrientationProvider +{ + boolean startOrientationProvider(IOrientationConsumer orientationConsumer); + + void stopOrientationProvider(); + + float getLastKnownOrientation(); +} diff --git a/src/main/java/org/osmdroid/views/overlay/compass/InternalCompassOrientationProvider.java b/src/main/java/org/osmdroid/views/overlay/compass/InternalCompassOrientationProvider.java new file mode 100644 index 000000000..9f3967250 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/compass/InternalCompassOrientationProvider.java @@ -0,0 +1,73 @@ +package org.osmdroid.views.overlay.compass; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +public class InternalCompassOrientationProvider implements SensorEventListener, IOrientationProvider +{ + private IOrientationConsumer mOrientationConsumer; + private final SensorManager mSensorManager; + private float mAzimuth; + + public InternalCompassOrientationProvider(Context context) + { + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + } + + // + // IOrientationProvider + // + + /** + * Enable orientation updates from the internal compass sensor and show the compass. + */ + @Override + public boolean startOrientationProvider(IOrientationConsumer orientationConsumer) + { + mOrientationConsumer = orientationConsumer; + boolean result = false; + + final Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); + if (sensor != null) { + result = mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI); + } + return result; + } + + @Override + public void stopOrientationProvider() + { + mSensorManager.unregisterListener(this); + } + + @Override + public float getLastKnownOrientation() + { + return mAzimuth; + } + + // + // SensorEventListener + // + + @Override + public void onAccuracyChanged(final Sensor sensor, final int accuracy) + { + // This is not interesting for us at the moment + } + + @Override + public void onSensorChanged(final SensorEvent event) + { + if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { + if (event.values != null) { + mAzimuth = event.values[0]; + if (mOrientationConsumer != null) + mOrientationConsumer.onOrientationChanged(mAzimuth, this); + } + } + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/mylocation/GpsMyLocationProvider.java b/src/main/java/org/osmdroid/views/overlay/mylocation/GpsMyLocationProvider.java new file mode 100644 index 000000000..1f8d76308 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/mylocation/GpsMyLocationProvider.java @@ -0,0 +1,120 @@ +package org.osmdroid.views.overlay.mylocation; + +import org.osmdroid.util.NetworkLocationIgnorer; + +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; + +public class GpsMyLocationProvider implements IMyLocationProvider, LocationListener { + private final LocationManager mLocationManager; + private Location mLocation; + + private IMyLocationConsumer mMyLocationConsumer; + private long mLocationUpdateMinTime = 0; + private float mLocationUpdateMinDistance = 0.0f; + private final NetworkLocationIgnorer mIgnorer = new NetworkLocationIgnorer(); + + public GpsMyLocationProvider(Context context) { + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + public long getLocationUpdateMinTime() { + return mLocationUpdateMinTime; + } + + /** + * Set the minimum interval for location updates. See {@link + * LocationManager.requestLocationUpdates(String, long, float, LocationListener)}. Note that you + * should call this before calling {@link enableMyLocation()}. + * + * @param milliSeconds + */ + public void setLocationUpdateMinTime(final long milliSeconds) { + mLocationUpdateMinTime = milliSeconds; + } + + public float getLocationUpdateMinDistance() { + return mLocationUpdateMinDistance; + } + + /** + * Set the minimum distance for location updates. See + * {@link LocationManager.requestLocationUpdates}. Note that you should call this before calling + * {@link enableMyLocation()}. + * + * @param meters + */ + public void setLocationUpdateMinDistance(final float meters) { + mLocationUpdateMinDistance = meters; + } + + // + // IMyLocationProvider + // + + /** + * Enable location updates and show your current location on the map. By default this will + * request location updates as frequently as possible, but you can change the frequency and/or + * distance by calling {@link setLocationUpdateMinTime(long)} and/or {@link + * setLocationUpdateMinDistance(float)} before calling this method. + */ + @Override + public boolean startLocationProvider(IMyLocationConsumer myLocationConsumer) { + mMyLocationConsumer = myLocationConsumer; + boolean result = false; + for (final String provider : mLocationManager.getProviders(true)) { + if (LocationManager.GPS_PROVIDER.equals(provider) + || LocationManager.NETWORK_PROVIDER.equals(provider)) { + result = true; + mLocationManager.requestLocationUpdates(provider, mLocationUpdateMinTime, + mLocationUpdateMinDistance, this); + } + } + return result; + } + + @Override + public void stopLocationProvider() { + mMyLocationConsumer = null; + mLocationManager.removeUpdates(this); + } + + @Override + public Location getLastKnownLocation() { + return mLocation; + } + + // + // LocationListener + // + + @Override + public void onLocationChanged(final Location location) { + // ignore temporary non-gps fix + if (mIgnorer.shouldIgnore(location.getProvider(), System.currentTimeMillis())) + return; + + mLocation = location; + if (mMyLocationConsumer != null) + mMyLocationConsumer.onLocationChanged(mLocation, this); + } + + @Override + public void onProviderDisabled(final String provider) { + } + + @Override + public void onProviderEnabled(final String provider) { + } + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) { + } +} diff --git a/src/main/java/org/osmdroid/views/overlay/mylocation/IMyLocationConsumer.java b/src/main/java/org/osmdroid/views/overlay/mylocation/IMyLocationConsumer.java new file mode 100644 index 000000000..7e19b2616 --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/mylocation/IMyLocationConsumer.java @@ -0,0 +1,7 @@ +package org.osmdroid.views.overlay.mylocation; + +import android.location.Location; + +public interface IMyLocationConsumer { + void onLocationChanged(Location location, IMyLocationProvider source); +} diff --git a/src/main/java/org/osmdroid/views/overlay/mylocation/IMyLocationProvider.java b/src/main/java/org/osmdroid/views/overlay/mylocation/IMyLocationProvider.java new file mode 100644 index 000000000..4b83e913b --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/mylocation/IMyLocationProvider.java @@ -0,0 +1,11 @@ +package org.osmdroid.views.overlay.mylocation; + +import android.location.Location; + +public interface IMyLocationProvider { + boolean startLocationProvider(IMyLocationConsumer myLocationConsumer); + + void stopLocationProvider(); + + Location getLastKnownLocation(); +} diff --git a/src/main/java/org/osmdroid/views/overlay/mylocation/MyLocationNewOverlay.java b/src/main/java/org/osmdroid/views/overlay/mylocation/MyLocationNewOverlay.java new file mode 100644 index 000000000..c69bc5b3c --- /dev/null +++ b/src/main/java/org/osmdroid/views/overlay/mylocation/MyLocationNewOverlay.java @@ -0,0 +1,549 @@ +package org.osmdroid.views.overlay.mylocation; + +import java.util.LinkedList; + +import org.osmdroid.DefaultResourceProxyImpl; +import org.osmdroid.ResourceProxy; +import org.osmdroid.api.IMapController; +import org.osmdroid.api.IMapView; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.util.TileSystem; +import org.osmdroid.views.MapView; +import org.osmdroid.views.MapView.Projection; +import org.osmdroid.views.overlay.IOverlayMenuProvider; +import org.osmdroid.views.overlay.Overlay.Snappable; +import org.osmdroid.views.overlay.SafeDrawOverlay; +import org.osmdroid.views.safecanvas.ISafeCanvas; +import org.osmdroid.views.safecanvas.SafePaint; +import org.osmdroid.views.util.constants.MapViewConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Paint.Style; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.location.Location; +import android.util.FloatMath; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; + +/** + * + * @author Marc Kurtz + * @author Manuel Stahl + * + */ +public class MyLocationNewOverlay extends SafeDrawOverlay implements IMyLocationConsumer, + IOverlayMenuProvider, Snappable { + private static final Logger logger = LoggerFactory.getLogger(MyLocationNewOverlay.class); + + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + protected final SafePaint mPaint = new SafePaint(); + protected final SafePaint mCirclePaint = new SafePaint(); + + protected final Bitmap mPersonBitmap; + protected final Bitmap mDirectionArrowBitmap; + + protected final MapView mMapView; + + private final IMapController mMapController; + public IMyLocationProvider mMyLocationProvider; + + private final LinkedList mRunOnFirstFix = new LinkedList(); + private final Point mMapCoords = new Point(); + + private Location mLocation; + private final GeoPoint mGeoPoint = new GeoPoint(0, 0); // for reuse + private boolean mIsLocationEnabled = false; + protected boolean mIsFollowing = false; // follow location updates + protected boolean mDrawAccuracyEnabled = true; + + /** Coordinates the feet of the person are located scaled for display density. */ + protected final PointF mPersonHotspot; + + protected final double mDirectionArrowCenterX; + protected final double mDirectionArrowCenterY; + + public static final int MENU_MY_LOCATION = getSafeMenuId(); + + private boolean mOptionsMenuEnabled = true; + + // to avoid allocations during onDraw + private final float[] mMatrixValues = new float[9]; + private final Matrix mMatrix = new Matrix(); + private final Rect mMyLocationRect = new Rect(); + private final Rect mMyLocationPreviousRect = new Rect(); + + // =========================================================== + // Constructors + // =========================================================== + + public MyLocationNewOverlay(Context context, MapView mapView) { + this(context, new GpsMyLocationProvider(context), mapView); + } + + public MyLocationNewOverlay(Context context, IMyLocationProvider myLocationProvider, + MapView mapView) { + this(myLocationProvider, mapView, new DefaultResourceProxyImpl(context)); + } + + public MyLocationNewOverlay(IMyLocationProvider myLocationProvider, MapView mapView, + ResourceProxy resourceProxy) { + super(resourceProxy); + + mMapView = mapView; + mMapController = mapView.getController(); + mCirclePaint.setARGB(0, 100, 100, 255); + mCirclePaint.setAntiAlias(true); + + mPersonBitmap = mResourceProxy.getBitmap(ResourceProxy.bitmap.person); + mDirectionArrowBitmap = mResourceProxy.getBitmap(ResourceProxy.bitmap.direction_arrow); + + mDirectionArrowCenterX = mDirectionArrowBitmap.getWidth() / 2.0 - 0.5; + mDirectionArrowCenterY = mDirectionArrowBitmap.getHeight() / 2.0 - 0.5; + + // Calculate position of person icon's feet, scaled to screen density + mPersonHotspot = new PointF(24.0f * mScale + 0.5f, 39.0f * mScale + 0.5f); + + setMyLocationProvider(myLocationProvider); + } + + @Override + public void onDetach(MapView mapView) { + this.disableMyLocation(); + super.onDetach(mapView); + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + /** + * If enabled, an accuracy circle will be drawn around your current position. + * + * @param drawAccuracyEnabled + * whether the accuracy circle will be enabled + */ + public void setDrawAccuracyEnabled(final boolean drawAccuracyEnabled) { + mDrawAccuracyEnabled = drawAccuracyEnabled; + } + + /** + * If enabled, an accuracy circle will be drawn around your current position. + * + * @return true if enabled, false otherwise + */ + public boolean isDrawAccuracyEnabled() { + return mDrawAccuracyEnabled; + } + + public IMyLocationProvider getMyLocationProvider() { + return mMyLocationProvider; + } + + protected void setMyLocationProvider(IMyLocationProvider myLocationProvider) { + if (myLocationProvider == null) + throw new RuntimeException( + "You must pass an IMyLocationProvider to setMyLocationProvider()"); + + if (mMyLocationProvider != null) + mMyLocationProvider.stopLocationProvider(); + + mMyLocationProvider = myLocationProvider; + } + + public void setPersonHotspot(float x, float y) { + mPersonHotspot.set(x, y); + } + + protected void drawMyLocation(final ISafeCanvas canvas, final MapView mapView, + final Location lastFix) { + final Projection pj = mapView.getProjection(); + final int zoomDiff = MapViewConstants.MAXIMUM_ZOOMLEVEL - pj.getZoomLevel(); + + if (mDrawAccuracyEnabled) { + final float radius = lastFix.getAccuracy() + / (float) TileSystem.GroundResolution(lastFix.getLatitude(), + mapView.getZoomLevel()); + + mCirclePaint.setAlpha(50); + mCirclePaint.setStyle(Style.FILL); + canvas.drawCircle(mMapCoords.x >> zoomDiff, mMapCoords.y >> zoomDiff, radius, + mCirclePaint); + + mCirclePaint.setAlpha(150); + mCirclePaint.setStyle(Style.STROKE); + canvas.drawCircle(mMapCoords.x >> zoomDiff, mMapCoords.y >> zoomDiff, radius, + mCirclePaint); + } + + canvas.getMatrix(mMatrix); + mMatrix.getValues(mMatrixValues); + + if (DEBUGMODE) { + final float tx = (-mMatrixValues[Matrix.MTRANS_X] + 20) + / mMatrixValues[Matrix.MSCALE_X]; + final float ty = (-mMatrixValues[Matrix.MTRANS_Y] + 90) + / mMatrixValues[Matrix.MSCALE_Y]; + canvas.drawText("Lat: " + lastFix.getLatitude(), tx, ty + 5, mPaint); + canvas.drawText("Lon: " + lastFix.getLongitude(), tx, ty + 20, mPaint); + canvas.drawText("Alt: " + lastFix.getAltitude(), tx, ty + 35, mPaint); + canvas.drawText("Acc: " + lastFix.getAccuracy(), tx, ty + 50, mPaint); + } + + // Calculate real scale including accounting for rotation + float scaleX = (float) Math.sqrt(mMatrixValues[Matrix.MSCALE_X] + * mMatrixValues[Matrix.MSCALE_X] + mMatrixValues[Matrix.MSKEW_Y] + * mMatrixValues[Matrix.MSKEW_Y]); + float scaleY = (float) Math.sqrt(mMatrixValues[Matrix.MSCALE_Y] + * mMatrixValues[Matrix.MSCALE_Y] + mMatrixValues[Matrix.MSKEW_X] + * mMatrixValues[Matrix.MSKEW_X]); + final double x = mMapCoords.x >> zoomDiff; + final double y = mMapCoords.y >> zoomDiff; + if (lastFix.hasBearing()) { + canvas.save(); + // Rotate the icon + canvas.rotate(lastFix.getBearing(), x, y); + // Counteract any scaling that may be happening so the icon stays the same size + canvas.scale(1 / scaleX, 1 / scaleY, x, y); + // Draw the bitmap + canvas.drawBitmap(mDirectionArrowBitmap, x - mDirectionArrowCenterX, y + - mDirectionArrowCenterY, mPaint); + canvas.restore(); + } else { + canvas.save(); + // Unrotate the icon if the maps are rotated so the little man stays upright + canvas.rotate(-mMapView.getMapOrientation(), x, y); + // Counteract any scaling that may be happening so the icon stays the same size + canvas.scale(1 / scaleX, 1 / scaleY, x, y); + // Draw the bitmap + canvas.drawBitmap(mPersonBitmap, x - mPersonHotspot.x, y - mPersonHotspot.y, mPaint); + canvas.restore(); + } + } + + protected Rect getMyLocationDrawingBounds(int zoomLevel, Location lastFix, Rect reuse) { + if (reuse == null) + reuse = new Rect(); + + final int zoomDiff = MapViewConstants.MAXIMUM_ZOOMLEVEL - zoomLevel; + final int posX = mMapCoords.x >> zoomDiff; + final int posY = mMapCoords.y >> zoomDiff; + + // Start with the bitmap bounds + if (lastFix.hasBearing()) { + // Get a square bounding box around the object, and expand by the length of the diagonal + // so as to allow for extra space for rotating + int widestEdge = (int) Math.ceil(Math.max(mDirectionArrowBitmap.getWidth(), + mDirectionArrowBitmap.getHeight()) * Math.sqrt(2)); + reuse.set(posX, posY, posX + widestEdge, posY + widestEdge); + reuse.offset(-widestEdge / 2, -widestEdge / 2); + } else { + reuse.set(posX, posY, posX + mPersonBitmap.getWidth(), posY + mPersonBitmap.getHeight()); + reuse.offset((int) (-mPersonHotspot.x + 0.5f), (int) (-mPersonHotspot.y + 0.5f)); + } + + // Add in the accuracy circle if enabled + if (mDrawAccuracyEnabled) { + final int radius = (int) FloatMath.ceil(lastFix.getAccuracy() + / (float) TileSystem.GroundResolution(lastFix.getLatitude(), zoomLevel)); + reuse.union(posX - radius, posY - radius, posX + radius, posY + radius); + final int strokeWidth = (int) FloatMath.ceil(mCirclePaint.getStrokeWidth() == 0 ? 1 + : mCirclePaint.getStrokeWidth()); + reuse.inset(-strokeWidth, -strokeWidth); + } + + return reuse; + } + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + @Override + protected void drawSafe(ISafeCanvas canvas, MapView mapView, boolean shadow) { + if (shadow) + return; + + if (mLocation != null && isMyLocationEnabled()) { + drawMyLocation(canvas, mapView, mLocation); + } + } + + @Override + public boolean onSnapToItem(final int x, final int y, final Point snapPoint, + final IMapView mapView) { + if (this.mLocation != null) { + snapPoint.x = mMapCoords.x; + snapPoint.y = mMapCoords.y; + final double xDiff = x - mMapCoords.x; + final double yDiff = y - mMapCoords.y; + final boolean snap = xDiff * xDiff + yDiff * yDiff < 64; + if (DEBUGMODE) { + logger.debug("snap=" + snap); + } + return snap; + } else { + return false; + } + } + + @Override + public boolean onTouchEvent(final MotionEvent event, final MapView mapView) { + if (event.getAction() == MotionEvent.ACTION_MOVE) { + this.disableFollowLocation(); + } + + return super.onTouchEvent(event, mapView); + } + + // =========================================================== + // Menu handling methods + // =========================================================== + + @Override + public void setOptionsMenuEnabled(final boolean pOptionsMenuEnabled) { + this.mOptionsMenuEnabled = pOptionsMenuEnabled; + } + + @Override + public boolean isOptionsMenuEnabled() { + return this.mOptionsMenuEnabled; + } + + @Override + public boolean onCreateOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView) { + pMenu.add(0, MENU_MY_LOCATION + pMenuIdOffset, Menu.NONE, + mResourceProxy.getString(ResourceProxy.string.my_location)) + .setIcon(mResourceProxy.getDrawable(ResourceProxy.bitmap.ic_menu_mylocation)) + .setCheckable(true); + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(final Menu pMenu, final int pMenuIdOffset, + final MapView pMapView) { + pMenu.findItem(MENU_MY_LOCATION + pMenuIdOffset).setChecked(this.isMyLocationEnabled()); + return false; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem pItem, final int pMenuIdOffset, + final MapView pMapView) { + final int menuId = pItem.getItemId() - pMenuIdOffset; + if (menuId == MENU_MY_LOCATION) { + if (this.isMyLocationEnabled()) { + this.disableFollowLocation(); + this.disableMyLocation(); + } else { + this.enableFollowLocation(); + this.enableMyLocation(); + } + return true; + } else { + return false; + } + } + + // =========================================================== + // Methods + // =========================================================== + + /** + * Return a GeoPoint of the last known location, or null if not known. + */ + public GeoPoint getMyLocation() { + if (mLocation == null) { + return null; + } else { + return new GeoPoint(mLocation); + } + } + + public Location getLastFix() { + return mLocation; + } + + /** + * Enables "follow" functionality. The map will center on your current location and + * automatically scroll as you move. Scrolling the map in the UI will disable. + */ + public void enableFollowLocation() { + mIsFollowing = true; + + // set initial location when enabled + if (isMyLocationEnabled()) { + mLocation = mMyLocationProvider.getLastKnownLocation(); + if (mLocation != null) { + TileSystem.LatLongToPixelXY(mLocation.getLatitude(), mLocation.getLongitude(), + MapViewConstants.MAXIMUM_ZOOMLEVEL, mMapCoords); + final int worldSize_2 = TileSystem.MapSize(MapViewConstants.MAXIMUM_ZOOMLEVEL) / 2; + mMapCoords.offset(-worldSize_2, -worldSize_2); + mMapController.animateTo(new GeoPoint(mLocation)); + } + } + + // Update the screen to see changes take effect + if (mMapView != null) { + mMapView.postInvalidate(); + } + } + + /** + * Disables "follow" functionality. + */ + public void disableFollowLocation() { + mIsFollowing = false; + } + + /** + * If enabled, the map will center on your current location and automatically scroll as you + * move. Scrolling the map in the UI will disable. + * + * @return true if enabled, false otherwise + */ + public boolean isFollowLocationEnabled() { + return mIsFollowing; + } + + @Override + public void onLocationChanged(Location location, IMyLocationProvider source) { + // If we had a previous location, let's get those bounds + Location oldLocation = mLocation; + if (oldLocation != null) { + this.getMyLocationDrawingBounds(mMapView.getZoomLevel(), oldLocation, + mMyLocationPreviousRect); + } + + mLocation = location; + mMapCoords.set(0, 0); + + if (mLocation != null) { + TileSystem.LatLongToPixelXY(mLocation.getLatitude(), mLocation.getLongitude(), + MapViewConstants.MAXIMUM_ZOOMLEVEL, mMapCoords); + final int worldSize_2 = TileSystem.MapSize(MapViewConstants.MAXIMUM_ZOOMLEVEL) / 2; + mMapCoords.offset(-worldSize_2, -worldSize_2); + + if (mIsFollowing) { + mGeoPoint.setLatitudeE6((int) (mLocation.getLatitude() * 1E6)); + mGeoPoint.setLongitudeE6((int) (mLocation.getLongitude() * 1E6)); + mMapController.animateTo(mGeoPoint); + } else { + // Get new drawing bounds + this.getMyLocationDrawingBounds(mMapView.getZoomLevel(), mLocation, mMyLocationRect); + + // If we had a previous location, merge in those bounds too + if (oldLocation != null) { + mMyLocationRect.union(mMyLocationPreviousRect); + } + + final int left = mMyLocationRect.left; + final int top = mMyLocationRect.top; + final int right = mMyLocationRect.right; + final int bottom = mMyLocationRect.bottom; + + // Invalidate the bounds + mMapView.post(new Runnable() { + @Override + public void run() { + mMapView.invalidateMapCoordinates(left, top, right, bottom); + } + }); + } + } + + for (final Runnable runnable : mRunOnFirstFix) { + new Thread(runnable).start(); + } + mRunOnFirstFix.clear(); + } + + public boolean enableMyLocation(IMyLocationProvider myLocationProvider) { + this.setMyLocationProvider(myLocationProvider); + mIsLocationEnabled = false; + return enableMyLocation(); + } + + /** + * Enable receiving location updates from the provided IMyLocationProvider and show your + * location on the maps. You will likely want to call enableMyLocation() from your Activity's + * Activity.onResume() method, to enable the features of this overlay. Remember to call the + * corresponding disableMyLocation() in your Activity's Activity.onPause() method to turn off + * updates when in the background. + */ + public boolean enableMyLocation() { + if (mIsLocationEnabled) + mMyLocationProvider.stopLocationProvider(); + + boolean result = mMyLocationProvider.startLocationProvider(this); + mIsLocationEnabled = result; + + // set initial location when enabled + if (result && isFollowLocationEnabled()) { + mLocation = mMyLocationProvider.getLastKnownLocation(); + if (mLocation != null) { + TileSystem.LatLongToPixelXY(mLocation.getLatitude(), mLocation.getLongitude(), + MapViewConstants.MAXIMUM_ZOOMLEVEL, mMapCoords); + final int worldSize_2 = TileSystem.MapSize(MapViewConstants.MAXIMUM_ZOOMLEVEL) / 2; + mMapCoords.offset(-worldSize_2, -worldSize_2); + mMapController.animateTo(new GeoPoint(mLocation)); + } + } + + // Update the screen to see changes take effect + if (mMapView != null) { + mMapView.postInvalidate(); + } + + return result; + } + + /** + * Disable location updates + */ + public void disableMyLocation() { + mIsLocationEnabled = false; + + if (mMyLocationProvider != null) { + mMyLocationProvider.stopLocationProvider(); + } + + // Update the screen to see changes take effect + if (mMapView != null) { + mMapView.postInvalidate(); + } + } + + /** + * If enabled, the map is receiving location updates and drawing your location on the map. + * + * @return true if enabled, false otherwise + */ + public boolean isMyLocationEnabled() { + return mIsLocationEnabled; + } + + public boolean runOnFirstFix(final Runnable runnable) { + if (mMyLocationProvider != null && mLocation != null) { + new Thread(runnable).start(); + return true; + } else { + mRunOnFirstFix.addLast(runnable); + return false; + } + } +} diff --git a/src/main/java/org/osmdroid/views/safecanvas/ISafeCanvas.java b/src/main/java/org/osmdroid/views/safecanvas/ISafeCanvas.java new file mode 100644 index 000000000..e3ca7308c --- /dev/null +++ b/src/main/java/org/osmdroid/views/safecanvas/ISafeCanvas.java @@ -0,0 +1,1079 @@ +package org.osmdroid.views.safecanvas; + + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Canvas.EdgeType; +import android.graphics.Canvas.VertexMode; +import android.graphics.DrawFilter; +import android.graphics.Matrix; +import android.graphics.Picture; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Region; + +/** + * The ISafeCanvas interface is designed to work Android's issues with large canvases.
+ *
+ * The internal representation of canvas coordinates in the Skia graphics library is float. Canvas + * sizes are specified as integers. At high zoom levels, the canvas sizes get to the high end of the + * integer data type which subsequently get rounded off when represented as floats in Skia. This + * causes drawing anomalies such as jagged edges or distorted shapes that progressively get worse as + * the zoom level increases. The issue becomes visibly noticeable around zoom level 18 which is + * commonly available amongst most online map tile providers.
+ *
+ * To prevent this issue we can't pass large values to the native Android methods. To accomplish + * that, we must intercept all values being passed to the canvas draw methods and translate them to + * a coordinate system where the origin (0,0) is the center of the screen. We then draw them to a + * canvas that has the same coordinate system. We also prevent passing coordinate parameter values + * as floats, instead accepting doubles.
+ * + * @see {@link SafeTranslatedCanvas}, {@link SafeTranslatedPath}, {@link SafePaint} + * + * @author Marc Kurtz + * + */ +public interface ISafeCanvas { + + /** + * Allows access to the original unsafe canvas. + */ + public interface UnsafeCanvasHandler { + void onUnsafeCanvas(Canvas canvas); + } + + /** + * Gets the x-offset that will be used to adjust all drawing values. + */ + public int getXOffset(); + + /** + * Gets the y-offset that will be used to adjust all drawing values. + */ + public int getYOffset(); + + /** + * Allows access to the original unsafe canvas through an {@link UnsafeCanvasHandler}. + */ + public void getUnsafeCanvas(UnsafeCanvasHandler handler); + + /** + * Gets the wrapped canvas. This canvas will have a coordinate system where the origin is at the + * center of the screen, but will not automatically adjust values passed to its drawing methods. + */ + public Canvas getWrappedCanvas(); + + /** + * Gets this safe canvas as an Android {@link Native} class. This canvas will have a coordinate + * system where the origin is at the center of the screen, and will automatically adjust values + * passed to its drawing methods by {@link #getXOffset()} and {@link #getYOffset()}. + */ + public Canvas getSafeCanvas(); + + /** + * Specify a bitmap for the canvas to draw into. As a side-effect, also updates the canvas's + * target density to match that of the bitmap. + * + * @param bitmap + * Specifies a mutable bitmap for the canvas to draw into. + * + * @see #setDensity(int) + * @see #getDensity() + */ + public abstract void setBitmap(Bitmap bitmap); + + /** + * Return true if the device that the current layer draws into is opaque (i.e. does not support + * per-pixel alpha). + * + * @return true if the device that the current layer draws into is opaque + */ + public abstract boolean isOpaque(); + + /** + * Returns the width of the current drawing layer + * + * @return the width of the current drawing layer + */ + public abstract int getWidth(); + + /** + * Returns the height of the current drawing layer + * + * @return the height of the current drawing layer + */ + public abstract int getHeight(); + + /** + *

+ * Returns the target density of the canvas. The default density is derived from the density of + * its backing bitmap, or {@link Bitmap#DENSITY_NONE} if there is not one. + *

+ * + * @return Returns the current target density of the canvas, which is used to determine the + * scaling factor when drawing a bitmap into it. + * + * @see #setDensity(int) + * @see Bitmap#getDensity() + */ + public abstract int getDensity(); + + /** + *

+ * Specifies the density for this Canvas' backing bitmap. This modifies the target density of + * the canvas itself, as well as the density of its backing bitmap via + * {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}. + * + * @param density + * The new target density of the canvas, which is used to determine the scaling + * factor when drawing a bitmap into it. Use {@link Bitmap#DENSITY_NONE} to disable + * bitmap scaling. + * + * @see #getDensity() + * @see Bitmap#setDensity(int) + */ + public abstract void setDensity(int density); + + /** + * Saves the current matrix and clip onto a private stack. Subsequent calls to + * translate,scale,rotate,skew,concat or clipRect,clipPath will all operate as usual, but when + * the balancing call to restore() is made, those calls will be forgotten, and the settings that + * existed before the save() will be reinstated. + * + * @return The value to pass to restoreToCount() to balance this save() + */ + public abstract int save(); + + /** + * Based on saveFlags, can save the current matrix and clip onto a private stack. Subsequent + * calls to translate,scale,rotate,skew,concat or clipRect,clipPath will all operate as usual, + * but when the balancing call to restore() is made, those calls will be forgotten, and the + * settings that existed before the save() will be reinstated. + * + * @param saveFlags + * flag bits that specify which parts of the Canvas state to save/restore + * @return The value to pass to restoreToCount() to balance this save() + */ + public abstract int save(int saveFlags); + + /** + * This behaves the same as save(), but in addition it allocates an offscreen bitmap. All + * drawing calls are directed there, and only when the balancing call to restore() is made is + * that offscreen transfered to the canvas (or the previous layer). Subsequent calls to + * translate, scale, rotate, skew, concat or clipRect, clipPath all operate on this copy. When + * the balancing call to restore() is made, this copy is deleted and the previous matrix/clip + * state is restored. + * + * @param bounds + * May be null. The maximum size the offscreen bitmap needs to be (in local + * coordinates) + * @param paint + * This is copied, and is applied to the offscreen when restore() is called. + * @param saveFlags + * see _SAVE_FLAG constants + * @return value to pass to restoreToCount() to balance this save() + */ + public abstract int saveLayer(Rect bounds, SafePaint paint, int saveFlags); + + /** + * Helper version of saveLayer() that takes 4 values rather than a RectF. + */ + public abstract int saveLayer(double left, double top, double right, double bottom, + SafePaint paint, int saveFlags); + + /** + * This behaves the same as save(), but in addition it allocates an offscreen bitmap. All + * drawing calls are directed there, and only when the balancing call to restore() is made is + * that offscreen transfered to the canvas (or the previous layer). Subsequent calls to + * translate, scale, rotate, skew, concat or clipRect, clipPath all operate on this copy. When + * the balancing call to restore() is made, this copy is deleted and the previous matrix/clip + * state is restored. + * + * @param bounds + * The maximum size the offscreen bitmap needs to be (in local coordinates) + * @param alpha + * The alpha to apply to the offscreen when when it is drawn during restore() + * @param saveFlags + * see _SAVE_FLAG constants + * @return value to pass to restoreToCount() to balance this call + */ + public abstract int saveLayerAlpha(Rect bounds, int alpha, int saveFlags); + + /** + * Helper for saveLayerAlpha() that takes 4 values instead of a RectF. + */ + public abstract int saveLayerAlpha(double left, double top, double right, double bottom, + int alpha, int saveFlags); + + /** + * This call balances a previous call to save(), and is used to remove all modifications to the + * matrix/clip state since the last save call. It is an error to call restore() more times than + * save() was called. + */ + public abstract void restore(); + + /** + * Returns the number of matrix/clip states on the Canvas' private stack. This will equal # + * save() calls - # restore() calls. + */ + public abstract int getSaveCount(); + + /** + * Efficient way to pop any calls to save() that happened after the save count reached + * saveCount. It is an error for saveCount to be less than 1. + * + * Example: int count = canvas.save(); ... // more calls potentially to save() + * canvas.restoreToCount(count); // now the canvas is back in the same state it was before the + * initial // call to save(). + * + * @param saveCount + * The save level to restore to. + */ + public abstract void restoreToCount(int saveCount); + + /** + * Preconcat the current matrix with the specified translation + * + * @param dx + * The distance to translate in X + * @param dy + * The distance to translate in Y + */ + public abstract void translate(float dx, float dy); + + /** + * Preconcat the current matrix with the specified scale. + * + * @param sx + * The amount to scale in X + * @param sy + * The amount to scale in Y + */ + public abstract void scale(float sx, float sy); + + /** + * Preconcat the current matrix with the specified scale. + * + * @param sx + * The amount to scale in X + * @param sy + * The amount to scale in Y + * @param px + * The x-coord for the pivot point (unchanged by the scale) + * @param py + * The y-coord for the pivot point (unchanged by the scale) + */ + public abstract void scale(float sx, float sy, double px, double py); + + /** + * Preconcat the current matrix with the specified rotation. + * + * @param degrees + * The amount to rotate, in degrees + */ + public abstract void rotate(float degrees); + + /** + * Preconcat the current matrix with the specified rotation. + * + * @param degrees + * The amount to rotate, in degrees + * @param px + * The x-coord for the pivot point (unchanged by the rotation) + * @param py + * The y-coord for the pivot point (unchanged by the rotation) + */ + public abstract void rotate(float degrees, double px, double py); + + /** + * Preconcat the current matrix with the specified skew. + * + * @param sx + * The amount to skew in X + * @param sy + * The amount to skew in Y + */ + public abstract void skew(float sx, float sy); + + /** + * Preconcat the current matrix with the specified matrix. + * + * @param matrix + * The matrix to preconcatenate with the current matrix + */ + public abstract void concat(Matrix matrix); + + /** + * Completely replace the current matrix with the specified matrix. If the matrix parameter is + * null, then the current matrix is reset to identity. + * + * @param matrix + * The matrix to replace the current matrix with. If it is null, set the current + * matrix to identity. + */ + public abstract void setMatrix(Matrix matrix); + + /** + * Return, in ctm, the current transformation matrix. This does not alter the matrix in the + * canvas, but just returns a copy of it. + */ + public abstract void getMatrix(Matrix ctm); + + /** + * Return a new matrix with a copy of the canvas' current transformation matrix. + */ + public abstract Matrix getMatrix(); + + /** + * Modify the current clip with the specified rectangle, which is expressed in local + * coordinates. + * + * @param rect + * The rectangle to intersect with the current clip. + * @param op + * How the clip is modified + * @return true if the resulting clip is non-empty + */ + public abstract boolean clipRect(Rect rect, Region.Op op); + + /** + * Intersect the current clip with the specified rectangle, which is expressed in local + * coordinates. + * + * @param rect + * The rectangle to intersect with the current clip. + * @return true if the resulting clip is non-empty + */ + public abstract boolean clipRect(Rect rect); + + /** + * Modify the current clip with the specified rectangle, which is expressed in local + * coordinates. + * + * @param left + * The left side of the rectangle to intersect with the current clip + * @param top + * The top of the rectangle to intersect with the current clip + * @param right + * The right side of the rectangle to intersect with the current clip + * @param bottom + * The bottom of the rectangle to intersect with the current clip + * @param op + * How the clip is modified + * @return true if the resulting clip is non-empty + */ + public abstract boolean clipRect(double left, double top, double right, double bottom, + Region.Op op); + + /** + * Intersect the current clip with the specified rectangle, which is expressed in local + * coordinates. + * + * @param left + * The left side of the rectangle to intersect with the current clip + * @param top + * The top of the rectangle to intersect with the current clip + * @param right + * The right side of the rectangle to intersect with the current clip + * @param bottom + * The bottom of the rectangle to intersect with the current clip + * @return true if the resulting clip is non-empty + */ + public abstract boolean clipRect(double left, double top, double right, double bottom); + + /** + * Intersect the current clip with the specified rectangle, which is expressed in local + * coordinates. + * + * @param left + * The left side of the rectangle to intersect with the current clip + * @param top + * The top of the rectangle to intersect with the current clip + * @param right + * The right side of the rectangle to intersect with the current clip + * @param bottom + * The bottom of the rectangle to intersect with the current clip + * @return true if the resulting clip is non-empty + */ + public abstract boolean clipRect(int left, int top, int right, int bottom); + + /** + * Modify the current clip with the specified path. + * + * @param path + * The path to operate on the current clip + * @param op + * How the clip is modified + * @return true if the resulting is non-empty + */ + public abstract boolean clipPath(SafeTranslatedPath path, Region.Op op); + + /** + * Intersect the current clip with the specified path. + * + * @param path + * The path to intersect with the current clip + * @return true if the resulting is non-empty + */ + public abstract boolean clipPath(SafeTranslatedPath path); + + /** + * Modify the current clip with the specified region. Note that unlike clipRect() and clipPath() + * which transform their arguments by the current matrix, clipRegion() assumes its argument is + * already in the coordinate system of the current layer's bitmap, and so not transformation is + * performed. + * + * @param region + * The region to operate on the current clip, based on op + * @param op + * How the clip is modified + * @return true if the resulting is non-empty + */ + public abstract boolean clipRegion(Region region, Region.Op op); + + /** + * Intersect the current clip with the specified region. Note that unlike clipRect() and + * clipPath() which transform their arguments by the current matrix, clipRegion() assumes its + * argument is already in the coordinate system of the current layer's bitmap, and so not + * transformation is performed. + * + * @param region + * The region to operate on the current clip, based on op + * @return true if the resulting is non-empty + */ + public abstract boolean clipRegion(Region region); + + public abstract DrawFilter getDrawFilter(); + + public abstract void setDrawFilter(DrawFilter filter); + + /** + * Return true if the specified rectangle, after being transformed by the current matrix, would + * lie completely outside of the current clip. Call this to check if an area you intend to draw + * into is clipped out (and therefore you can skip making the draw calls). + * + * @param rect + * the rect to compare with the current clip + * @param type + * specifies how to treat the edges (BW or antialiased) + * @return true if the rect (transformed by the canvas' matrix) does not intersect with the + * canvas' clip + */ + public abstract boolean quickReject(Rect rect, EdgeType type); + + /** + * Return true if the specified path, after being transformed by the current matrix, would lie + * completely outside of the current clip. Call this to check if an area you intend to draw into + * is clipped out (and therefore you can skip making the draw calls). Note: for speed it may + * return false even if the path itself might not intersect the clip (i.e. the bounds of the + * path intersects, but the path does not). + * + * @param path + * The path to compare with the current clip + * @param type + * true if the path should be considered antialiased, since that means it may affect + * a larger area (more pixels) than non-antialiased. + * @return true if the path (transformed by the canvas' matrix) does not intersect with the + * canvas' clip + */ + public abstract boolean quickReject(SafeTranslatedPath path, EdgeType type); + + /** + * Return true if the specified rectangle, after being transformed by the current matrix, would + * lie completely outside of the current clip. Call this to check if an area you intend to draw + * into is clipped out (and therefore you can skip making the draw calls). + * + * @param left + * The left side of the rectangle to compare with the current clip + * @param top + * The top of the rectangle to compare with the current clip + * @param right + * The right side of the rectangle to compare with the current clip + * @param bottom + * The bottom of the rectangle to compare with the current clip + * @param type + * true if the rect should be considered antialiased, since that means it may affect + * a larger area (more pixels) than non-antialiased. + * @return true if the rect (transformed by the canvas' matrix) does not intersect with the + * canvas' clip + */ + public abstract boolean quickReject(double left, double top, double right, double bottom, + EdgeType type); + + /** + * Retrieve the clip bounds, returning true if they are non-empty. + * + * @param bounds + * Return the clip bounds here. If it is null, ignore it but still return true if the + * current clip is non-empty. + * @return true if the current clip is non-empty. + */ + public abstract boolean getClipBounds(Rect bounds); + + /** + * Retrieve the clip bounds. + * + * @return the clip bounds, or [0, 0, 0, 0] if the clip is empty. + */ + public abstract Rect getClipBounds(); + + /** + * Fill the entire canvas' bitmap (restricted to the current clip) with the specified RGB color, + * using srcover porterduff mode. + * + * @param r + * red component (0..255) of the color to draw onto the canvas + * @param g + * green component (0..255) of the color to draw onto the canvas + * @param b + * blue component (0..255) of the color to draw onto the canvas + */ + public abstract void drawRGB(int r, int g, int b); + + /** + * Fill the entire canvas' bitmap (restricted to the current clip) with the specified ARGB + * color, using srcover porterduff mode. + * + * @param a + * alpha component (0..255) of the color to draw onto the canvas + * @param r + * red component (0..255) of the color to draw onto the canvas + * @param g + * green component (0..255) of the color to draw onto the canvas + * @param b + * blue component (0..255) of the color to draw onto the canvas + */ + public abstract void drawARGB(int a, int r, int g, int b); + + /** + * Fill the entire canvas' bitmap (restricted to the current clip) with the specified color, + * using srcover porterduff mode. + * + * @param color + * the color to draw onto the canvas + */ + public abstract void drawColor(int color); + + /** + * Fill the entire canvas' bitmap (restricted to the current clip) with the specified color and + * porter-duff xfermode. + * + * @param color + * the color to draw with + * @param mode + * the porter-duff mode to apply to the color + */ + public abstract void drawColor(int color, PorterDuff.Mode mode); + + /** + * Fill the entire canvas' bitmap (restricted to the current clip) with the specified paint. + * This is equivalent (but faster) to drawing an infinitely large rectangle with the specified + * paint. + * + * @param paint + * The paint used to draw onto the canvas + */ + public abstract void drawPaint(SafePaint paint); + + /** + * Draw a series of points. Each point is centered at the coordinate specified by pts[], and its + * diameter is specified by the paint's stroke width (as transformed by the canvas' CTM), with + * special treatment for a stroke width of 0, which always draws exactly 1 pixel (or at most 4 + * if antialiasing is enabled). The shape of the point is controlled by the paint's Cap type. + * The shape is a square, unless the cap type is Round, in which case the shape is a circle. + * + * @param pts + * Array of points to draw [x0 y0 x1 y1 x2 y2 ...] + * @param offset + * Number of values to skip before starting to draw. + * @param count + * The number of values to process, after skipping offset of them. Since one point + * uses two values, the number of "points" that are drawn is really (count >> 1). + * @param paint + * The paint used to draw the points + */ + public abstract void drawPoints(double[] pts, int offset, int count, SafePaint paint); + + /** + * Helper for drawPoints() that assumes you want to draw the entire array + */ + public abstract void drawPoints(double[] pts, SafePaint paint); + + /** + * Helper for drawPoints() for drawing a single point. + */ + public abstract void drawPoint(double x, double y, SafePaint paint); + + /** + * Draw a line segment with the specified start and stop x,y coordinates, using the specified + * paint. NOTE: since a line is always "framed", the Style is ignored in the paint. + * + * @param startX + * The x-coordinate of the start point of the line + * @param startY + * The y-coordinate of the start point of the line + * @param paint + * The paint used to draw the line + */ + public abstract void drawLine(double startX, double startY, double stopX, double stopY, + SafePaint paint); + + /** + * Draw a series of lines. Each line is taken from 4 consecutive values in the pts array. Thus + * to draw 1 line, the array must contain at least 4 values. This is logically the same as + * drawing the array as follows: drawLine(pts[0], pts[1], pts[2], pts[3]) followed by + * drawLine(pts[4], pts[5], pts[6], pts[7]) and so on. + * + * @param pts + * Array of points to draw [x0 y0 x1 y1 x2 y2 ...] + * @param offset + * Number of values in the array to skip before drawing. + * @param count + * The number of values in the array to process, after skipping "offset" of them. + * Since each line uses 4 values, the number of "lines" that are drawn is really + * (count >> 2). + * @param paint + * The paint used to draw the points + */ + public abstract void drawLines(double[] pts, int offset, int count, SafePaint paint); + + public abstract void drawLines(double[] pts, SafePaint paint); + + /** + * Draw the specified Rect using the specified Paint. The rectangle will be filled or framed + * based on the Style in the paint. + * + * @param r + * The rectangle to be drawn. + * @param paint + * The paint used to draw the rectangle + */ + public abstract void drawRect(Rect r, SafePaint paint); + + /** + * Draw the specified Rect using the specified paint. The rectangle will be filled or framed + * based on the Style in the paint. + * + * @param left + * The left side of the rectangle to be drawn + * @param top + * The top side of the rectangle to be drawn + * @param right + * The right side of the rectangle to be drawn + * @param bottom + * The bottom side of the rectangle to be drawn + * @param paint + * The paint used to draw the rect + */ + public abstract void drawRect(double left, double top, double right, double bottom, + SafePaint paint); + + /** + * Draw the specified oval using the specified paint. The oval will be filled or framed based on + * the Style in the paint. + * + * @param oval + * The rectangle bounds of the oval to be drawn + */ + public abstract void drawOval(Rect oval, SafePaint paint); + + /** + * Draw the specified circle using the specified paint. If radius is <= 0, then nothing will be + * drawn. The circle will be filled or framed based on the Style in the paint. + * + * @param cx + * The x-coordinate of the center of the cirle to be drawn + * @param cy + * The y-coordinate of the center of the cirle to be drawn + * @param radius + * The radius of the cirle to be drawn + * @param paint + * The paint used to draw the circle + */ + public abstract void drawCircle(double cx, double cy, float radius, SafePaint paint); + + /** + *

+ * Draw the specified arc, which will be scaled to fit inside the specified oval. + *

+ * + *

+ * If the start angle is negative or >= 360, the start angle is treated as start angle modulo + * 360. + *

+ * + *

+ * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs + * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is + * negative, the sweep angle is treated as sweep angle modulo 360 + *

+ * + *

+ * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0 + * degrees (3 o'clock on a watch.) + *

+ * + * @param oval + * The bounds of oval used to define the shape and size of the arc + * @param startAngle + * Starting angle (in degrees) where the arc begins + * @param sweepAngle + * Sweep angle (in degrees) measured clockwise + * @param useCenter + * If true, include the center of the oval in the arc, and close it if it is being + * stroked. This will draw a wedge + * @param paint + * The paint used to draw the arc + */ + public abstract void drawArc(Rect oval, float startAngle, float sweepAngle, boolean useCenter, + SafePaint paint); + + /** + * Draw the specified round-rect using the specified paint. The roundrect will be filled or + * framed based on the Style in the paint. + * + * @param rect + * The rectangular bounds of the roundRect to be drawn + * @param rx + * The x-radius of the oval used to round the corners + * @param ry + * The y-radius of the oval used to round the corners + * @param paint + * The paint used to draw the roundRect + */ + public abstract void drawRoundRect(Rect rect, float rx, float ry, SafePaint paint); + + /** + * Draw the specified path using the specified paint. The path will be filled or framed based on + * the Style in the paint. + * + * @param path + * The path to be drawn + * @param paint + * The paint used to draw the path + */ + public abstract void drawPath(SafeTranslatedPath path, SafePaint paint); + + /** + * Draw the specified bitmap, with its top/left corner at (x,y), using the specified paint, + * transformed by the current matrix. + * + *

+ * Note: if the paint contains a maskfilter that generates a mask which extends beyond the + * bitmap's original width/height (e.g. BlurMaskFilter), then the bitmap will be drawn as if it + * were in a Shader with CLAMP mode. Thus the color outside of the original width/height will be + * the edge color replicated. + * + *

+ * If the bitmap and canvas have different densities, this function will take care of + * automatically scaling the bitmap to draw at the same density as the canvas. + * + * @param bitmap + * The bitmap to be drawn + * @param left + * The position of the left side of the bitmap being drawn + * @param top + * The position of the top side of the bitmap being drawn + * @param paint + * The paint used to draw the bitmap (may be null) + */ + public abstract void drawBitmap(Bitmap bitmap, double left, double top, SafePaint paint); + + /** + * Draw the specified bitmap, scaling/translating automatically to fill the destination + * rectangle. If the source rectangle is not null, it specifies the subset of the bitmap to + * draw. + * + *

+ * Note: if the paint contains a maskfilter that generates a mask which extends beyond the + * bitmap's original width/height (e.g. BlurMaskFilter), then the bitmap will be drawn as if it + * were in a Shader with CLAMP mode. Thus the color outside of the original width/height will be + * the edge color replicated. + * + *

+ * This function ignores the density associated with the bitmap. This is because the + * source and destination rectangle coordinate spaces are in their respective densities, so must + * already have the appropriate scaling factor applied. + * + * @param bitmap + * The bitmap to be drawn + * @param src + * May be null. The subset of the bitmap to be drawn + * @param dst + * The rectangle that the bitmap will be scaled/translated to fit into + * @param paint + * May be null. The paint used to draw the bitmap + */ + public abstract void drawBitmap(Bitmap bitmap, Rect src, Rect dst, SafePaint paint); + + /** + * Treat the specified array of colors as a bitmap, and draw it. This gives the same result as + * first creating a bitmap from the array, and then drawing it, but this method avoids + * explicitly creating a bitmap object which can be more efficient if the colors are changing + * often. + * + * @param colors + * Array of colors representing the pixels of the bitmap + * @param offset + * Offset into the array of colors for the first pixel + * @param stride + * The number of colors in the array between rows (must be >= width or <= -width). + * @param x + * The X coordinate for where to draw the bitmap + * @param y + * The Y coordinate for where to draw the bitmap + * @param width + * The width of the bitmap + * @param height + * The height of the bitmap + * @param hasAlpha + * True if the alpha channel of the colors contains valid values. If false, the alpha + * byte is ignored (assumed to be 0xFF for every pixel). + * @param paint + * May be null. The paint used to draw the bitmap + */ + public abstract void drawBitmap(int[] colors, int offset, int stride, double x, double y, + int width, int height, boolean hasAlpha, SafePaint paint); + + /** + * Legacy version of drawBitmap(int[] colors, ...) that took ints for x,y + */ + public abstract void drawBitmap(int[] colors, int offset, int stride, int x, int y, int width, + int height, boolean hasAlpha, SafePaint paint); + + /** + * Draw the bitmap using the specified matrix. + * + * @param bitmap + * The bitmap to draw + * @param matrix + * The matrix used to transform the bitmap when it is drawn + * @param paint + * May be null. The paint used to draw the bitmap + */ + public abstract void drawBitmap(Bitmap bitmap, Matrix matrix, SafePaint paint); + + /** + * Draw the bitmap through the mesh, where mesh vertices are evenly distributed across the + * bitmap. There are meshWidth+1 vertices across, and meshHeight+1 vertices down. The verts + * array is accessed in row-major order, so that the first meshWidth+1 vertices are distributed + * across the top of the bitmap from left to right. A more general version of this methid is + * drawVertices(). + * + * @param bitmap + * The bitmap to draw using the mesh + * @param meshWidth + * The number of columns in the mesh. Nothing is drawn if this is 0 + * @param meshHeight + * The number of rows in the mesh. Nothing is drawn if this is 0 + * @param verts + * Array of x,y pairs, specifying where the mesh should be drawn. There must be at + * least (meshWidth+1) * (meshHeight+1) * 2 + meshOffset values in the array + * @param vertOffset + * Number of verts elements to skip before drawing + * @param colors + * May be null. Specifies a color at each vertex, which is interpolated across the + * cell, and whose values are multiplied by the corresponding bitmap colors. If not + * null, there must be at least (meshWidth+1) * (meshHeight+1) + colorOffset values + * in the array. + * @param colorOffset + * Number of color elements to skip before drawing + * @param paint + * May be null. The paint used to draw the bitmap + */ + public abstract void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, + double[] verts, int vertOffset, int[] colors, int colorOffset, SafePaint paint); + + /** + * Draw the array of vertices, interpreted as triangles (based on mode). The verts array is + * required, and specifies the x,y pairs for each vertex. If texs is non-null, then it is used + * to specify the coordinate in shader coordinates to use at each vertex (the paint must have a + * shader in this case). If there is no texs array, but there is a color array, then each color + * is interpolated across its corresponding triangle in a gradient. If both texs and colors + * arrays are present, then they behave as before, but the resulting color at each pixels is the + * result of multiplying the colors from the shader and the color-gradient together. The indices + * array is optional, but if it is present, then it is used to specify the index of each + * triangle, rather than just walking through the arrays in order. + * + * @param mode + * How to interpret the array of vertices + * @param vertexCount + * The number of values in the vertices array (and corresponding texs and colors + * arrays if non-null). Each logical vertex is two values (x, y), vertexCount must be + * a multiple of 2. + * @param verts + * Array of vertices for the mesh + * @param vertOffset + * Number of values in the verts to skip before drawing. + * @param texs + * May be null. If not null, specifies the coordinates to sample into the current + * shader (e.g. bitmap tile or gradient) + * @param texOffset + * Number of values in texs to skip before drawing. + * @param colors + * May be null. If not null, specifies a color for each vertex, to be interpolated + * across the triangle. + * @param colorOffset + * Number of values in colors to skip before drawing. + * @param indices + * If not null, array of indices to reference into the vertex (texs, colors) array. + * @param indexCount + * number of entries in the indices array (if not null). + * @param paint + * Specifies the shader to use if the texs array is non-null. + */ + public abstract void drawVertices(VertexMode mode, int vertexCount, double[] verts, + int vertOffset, float[] texs, int texOffset, int[] colors, int colorOffset, + short[] indices, int indexOffset, int indexCount, SafePaint paint); + + /** + * Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted + * based on the Align setting in the paint. + * + * @param text + * The text to be drawn + * @param x + * The x-coordinate of the origin of the text being drawn + * @param y + * @param paint + * The paint used for the text (e.g. color, size, style) + */ + public abstract void drawText(char[] text, int index, int count, double x, double y, + SafePaint paint); + + /** + * Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted + * based on the Align setting in the paint. + * + * @param text + * The text to be drawn + * @param x + * The x-coordinate of the origin of the text being drawn + * @param y + * The y-coordinate of the origin of the text being drawn + * @param paint + * The paint used for the text (e.g. color, size, style) + */ + public abstract void drawText(String text, double x, double y, SafePaint paint); + + /** + * Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted + * based on the Align setting in the paint. + * + * @param text + * The text to be drawn + * @param start + * The index of the first character in text to draw + * @param end + * (end - 1) is the index of the last character in text to draw + * @param x + * The x-coordinate of the origin of the text being drawn + * @param y + * The y-coordinate of the origin of the text being drawn + * @param paint + * The paint used for the text (e.g. color, size, style) + */ + public abstract void drawText(String text, int start, int end, double x, double y, + SafePaint paint); + + /** + * Draw the specified range of text, specified by start/end, with its origin at (x,y), in the + * specified Paint. The origin is interpreted based on the Align setting in the Paint. + * + * @param text + * The text to be drawn + * @param start + * The index of the first character in text to draw + * @param end + * (end - 1) is the index of the last character in text to draw + * @param x + * The x-coordinate of origin for where to draw the text + * @param y + * The y-coordinate of origin for where to draw the text + * @param paint + * The paint used for the text (e.g. color, size, style) + */ + public abstract void drawText(CharSequence text, int start, int end, double x, double y, + SafePaint paint); + + /** + * Draw the text in the array, with each character's origin specified by the pos array. + * + * @param text + * The text to be drawn + * @param index + * The index of the first character to draw + * @param count + * The number of characters to draw, starting from index. + * @param pos + * Array of [x,y] positions, used to position each character + * @param paint + * The paint used for the text (e.g. color, size, style) + */ + public abstract void drawPosText(char[] text, int index, int count, double[] pos, + SafePaint paint); + + /** + * Draw the text in the array, with each character's origin specified by the pos array. + * + * @param text + * The text to be drawn + * @param pos + * Array of [x,y] positions, used to position each character + * @param paint + * The paint used for the text (e.g. color, size, style) + */ + public abstract void drawPosText(String text, double[] pos, SafePaint paint); + + /** + * Draw the text, with origin at (x,y), using the specified paint, along the specified path. The + * paint's Align setting determins where along the path to start the text. + * + * @param text + * The text to be drawn + * @param path + * The path the text should follow for its baseline + * @param hOffset + * The distance along the path to add to the text's starting position + * @param vOffset + * The distance above(-) or below(+) the path to position the text + * @param paint + * The paint used for the text (e.g. color, size, style) + */ + public abstract void drawTextOnPath(char[] text, int index, int count, SafeTranslatedPath path, + float hOffset, float vOffset, SafePaint paint); + + /** + * Draw the text, with origin at (x,y), using the specified paint, along the specified path. The + * paint's Align setting determins where along the path to start the text. + * + * @param text + * The text to be drawn + * @param path + * The path the text should follow for its baseline + * @param hOffset + * The distance along the path to add to the text's starting position + * @param vOffset + * The distance above(-) or below(+) the path to position the text + * @param paint + * The paint used for the text (e.g. color, size, style) + */ + public abstract void drawTextOnPath(String text, SafeTranslatedPath path, float hOffset, + float vOffset, + SafePaint paint); + + /** + * Save the canvas state, draw the picture, and restore the canvas state. This differs from + * picture.draw(canvas), which does not perform any save/restore. + * + * @param picture + * The picture to be drawn + */ + public abstract void drawPicture(Picture picture); + + /** + * Draw the picture, stretched to fit into the dst rectangle. + */ + public abstract void drawPicture(Picture picture, Rect dst); + +} \ No newline at end of file diff --git a/src/main/java/org/osmdroid/views/safecanvas/SafeBitmapShader.java b/src/main/java/org/osmdroid/views/safecanvas/SafeBitmapShader.java new file mode 100644 index 000000000..091301e5e --- /dev/null +++ b/src/main/java/org/osmdroid/views/safecanvas/SafeBitmapShader.java @@ -0,0 +1,42 @@ +package org.osmdroid.views.safecanvas; + +import org.osmdroid.views.overlay.Overlay; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Matrix; + +/** + * The SafeBitmapShader class is designed to work in conjunction with {@link SafeTranslatedCanvas} + * to work around various Android issues with large canvases. For the two classes to work together, + * call {@link #onDrawCycleStart} at the start of the {@link Overlay#drawSafe} method of your + * {@link Overlay}. This will set the adjustment needed to draw your BitmapShader safely on the + * canvas without any drawing distortion at high zoom levels and without any scrolling issues. + * + * @see {@link ISafeCanvas} + * + * @author Marc Kurtz + * + */ +public class SafeBitmapShader extends BitmapShader { + + private final Matrix mMatrix = new Matrix(); + private final int mBitmapWidth; + private final int mBitmapHeight; + + public SafeBitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY) { + super(bitmap, tileX, tileY); + mBitmapWidth = bitmap.getWidth(); + mBitmapHeight = bitmap.getHeight(); + } + + /** + * This method must be called at the start of the {@link Overlay#drawSafe} draw cycle + * method. This will adjust the BitmapShader to the current state of the {@link ISafeCanvas} + * passed to it. + */ + public void onDrawCycleStart(ISafeCanvas canvas) { + mMatrix.setTranslate(canvas.getXOffset() % mBitmapWidth, canvas.getYOffset() % mBitmapHeight); + this.setLocalMatrix(mMatrix); + } +} diff --git a/src/main/java/org/osmdroid/views/safecanvas/SafeDashPathEffect.java b/src/main/java/org/osmdroid/views/safecanvas/SafeDashPathEffect.java new file mode 100644 index 000000000..7ae3fc2c6 --- /dev/null +++ b/src/main/java/org/osmdroid/views/safecanvas/SafeDashPathEffect.java @@ -0,0 +1,43 @@ +package org.osmdroid.views.safecanvas; + +import android.graphics.Path; +import android.graphics.PathDashPathEffect; + +public class SafeDashPathEffect extends PathDashPathEffect +{ + public SafeDashPathEffect(float[] intervals, float phase, float strokeWidth) + { + super(createSafeDashedPath(intervals, phase, strokeWidth, null), floatSum(intervals), + phase, PathDashPathEffect.Style.MORPH); + } + + public static Path createSafeDashedPath(float[] intervals, float phase, float strokeWidth, + Path reuse) + { + if (reuse == null) + reuse = new Path(); + + reuse.reset(); + reuse.moveTo(0, 0); + for (int a = 0; a < intervals.length; a++) { + if (a % 2 == 0) { + reuse.rMoveTo(0, strokeWidth / 2); + reuse.rLineTo(intervals[a], 0); + reuse.rLineTo(0, -strokeWidth); + reuse.rLineTo(-intervals[a], 0); + reuse.rLineTo(0, strokeWidth / 2); + reuse.rMoveTo(intervals[a], 0); + } else { + reuse.rMoveTo(intervals[a], 0); + } + } + return reuse; + } + + private static float floatSum(float[] array) { + float result = 0; + for (int a = 0; a < array.length; a++) + result += array[a]; + return result; + } +} diff --git a/src/main/java/org/osmdroid/views/safecanvas/SafePaint.java b/src/main/java/org/osmdroid/views/safecanvas/SafePaint.java new file mode 100644 index 000000000..516a3120b --- /dev/null +++ b/src/main/java/org/osmdroid/views/safecanvas/SafePaint.java @@ -0,0 +1,25 @@ +package org.osmdroid.views.safecanvas; + + +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.PathEffect; + +/** + * The SafePaint class is designed to work in conjunction with {@link SafeTranslatedCanvas} to work + * around various Android issues with large canvases. + * + * @see {@link ISafeCanvas} + * + * @author Marc Kurtz + * + */ +public class SafePaint extends Paint { + + @Override + public PathEffect setPathEffect(PathEffect effect) { + if (effect instanceof DashPathEffect) + throw new RuntimeException("Do not use DashPathEffect. Use SafeDashPathEffect instead."); + return super.setPathEffect(effect); + } +} diff --git a/src/main/java/org/osmdroid/views/safecanvas/SafeTranslatedCanvas.java b/src/main/java/org/osmdroid/views/safecanvas/SafeTranslatedCanvas.java new file mode 100644 index 000000000..3ca76929c --- /dev/null +++ b/src/main/java/org/osmdroid/views/safecanvas/SafeTranslatedCanvas.java @@ -0,0 +1,580 @@ +package org.osmdroid.views.safecanvas; + + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.DrawFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Picture; +import android.graphics.PorterDuff.Mode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.Region.Op; + +/** + * An implementation of {@link ISafeCanvas} that wraps a {@link Canvas} and adjusts drawing calls to + * the wrapped Canvas so that they are relative to an origin that is always at the center of the + * screen.
+ *
+ * See {@link ISafeCanvas} for details
+ * + * @author Marc Kurtz + * + */ +public class SafeTranslatedCanvas extends Canvas implements ISafeCanvas { + private final static Matrix sMatrix = new Matrix(); + private final static RectF sRectF = new RectF(); + private static float[] sFloatAry = new float[0]; + private Canvas mCanvas; + private final Matrix mMatrix = new Matrix(); + public int xOffset; + public int yOffset; + + public SafeTranslatedCanvas() { + // + } + + public SafeTranslatedCanvas(Canvas canvas) { + this.setCanvas(canvas); + } + + @Override + public Canvas getSafeCanvas() { + return this; + } + + @Override + public int getXOffset() { + return xOffset; + } + + @Override + public int getYOffset() { + return yOffset; + } + + public void setCanvas(Canvas canvas) { + mCanvas = canvas; + canvas.getMatrix(mMatrix); + } + + public void getUnsafeCanvas(UnsafeCanvasHandler handler) { + this.save(); + this.setMatrix(this.getOriginalMatrix()); + handler.onUnsafeCanvas(mCanvas); + this.restore(); + } + + public Canvas getWrappedCanvas() { + return mCanvas; + } + + public Matrix getOriginalMatrix() { + return mMatrix; + } + + @Override + public boolean clipPath(SafeTranslatedPath path, Op op) { + return getWrappedCanvas().clipPath(path, op); + } + + @Override + public boolean clipPath(SafeTranslatedPath path) { + return getWrappedCanvas().clipPath(path); + } + + @Override + public boolean clipRect(double left, double top, double right, double bottom, Op op) { + return getWrappedCanvas().clipRect((float) (left + xOffset), (float) (top + yOffset), + (float) (right + xOffset), (float) (bottom + yOffset), op); + } + + @Override + public boolean clipRect(double left, double top, double right, double bottom) { + return getWrappedCanvas().clipRect((float) (left + xOffset), (float) (top + yOffset), + (float) (right + xOffset), (float) (bottom + yOffset)); + } + + @Override + public boolean clipRect(int left, int top, int right, int bottom) { + return getWrappedCanvas().clipRect(left + xOffset, top + yOffset, right + xOffset, + bottom + yOffset); + } + + @Override + public boolean clipRect(Rect rect, Op op) { + rect.offset(xOffset, yOffset); + return getWrappedCanvas().clipRect(rect, op); + } + + @Override + public boolean clipRect(Rect rect) { + rect.offset(xOffset, yOffset); + return getWrappedCanvas().clipRect(rect); + } + + @Override + public boolean clipRegion(Region region, Op op) { + region.translate(xOffset, yOffset); + return getWrappedCanvas().clipRegion(region, op); + } + + @Override + public boolean clipRegion(Region region) { + region.translate(xOffset, yOffset); + return getWrappedCanvas().clipRegion(region); + } + + @Override + public void concat(Matrix matrix) { + getWrappedCanvas().concat(matrix); + } + + @Override + public void drawARGB(int a, int r, int g, int b) { + getWrappedCanvas().drawARGB(a, r, g, b); + } + + @Override + public void drawArc(Rect oval, float startAngle, float sweepAngle, boolean useCenter, + SafePaint paint) { + getWrappedCanvas().drawArc(this.toOffsetRectF(oval, sRectF), startAngle, sweepAngle, + useCenter, paint); + } + + @Override + public void drawBitmap(Bitmap bitmap, double left, double top, SafePaint paint) { + getWrappedCanvas().drawBitmap(bitmap, (float) (left + xOffset), (float) (top + yOffset), + paint); + } + + @Override + public void drawBitmap(Bitmap bitmap, Matrix matrix, SafePaint paint) { + sMatrix.set(matrix); + sMatrix.postTranslate(xOffset, yOffset); + getWrappedCanvas().drawBitmap(bitmap, sMatrix, paint); + } + + @Override + public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, SafePaint paint) { + dst.offset(xOffset, yOffset); + getWrappedCanvas().drawBitmap(bitmap, src, dst, paint); + dst.offset(-xOffset, -yOffset); + } + + /* This is used by Drawable.draw(Canvas), so also we adjust here */ + @Override + public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { + dst.offset(xOffset, yOffset); + getWrappedCanvas().drawBitmap(bitmap, src, dst, paint); + dst.offset(-xOffset, -yOffset); + } + + @Override + public void drawBitmap(int[] colors, int offset, int stride, double x, double y, int width, + int height, boolean hasAlpha, SafePaint paint) { + getWrappedCanvas().drawBitmap(colors, offset, stride, (float) (x + xOffset), + (float) (y + yOffset), width, height, hasAlpha, paint); + } + + @Override + public void drawBitmap(int[] colors, int offset, int stride, int x, int y, int width, + int height, boolean hasAlpha, SafePaint paint) { + getWrappedCanvas().drawBitmap(colors, offset, stride, x + offset, y + offset, width, + height, hasAlpha, paint); + } + + @Override + public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, double[] verts, + int vertOffset, int[] colors, int colorOffset, SafePaint paint) { + getWrappedCanvas().drawBitmapMesh(bitmap, meshWidth, meshHeight, + this.toOffsetFloatAry(verts, sFloatAry), vertOffset, colors, colorOffset, paint); + } + + @Override + public void drawCircle(double cx, double cy, float radius, SafePaint paint) { + getWrappedCanvas() + .drawCircle((float) (cx + xOffset), (float) (cy + yOffset), radius, paint); + } + + @Override + public void drawColor(int color, Mode mode) { + + getWrappedCanvas().drawColor(color, mode); + } + + @Override + public void drawColor(int color) { + + getWrappedCanvas().drawColor(color); + } + + @Override + public void drawLine(double startX, double startY, double stopX, double stopY, SafePaint paint) { + startX += xOffset; + startY += yOffset; + stopX += xOffset; + stopY += yOffset; + getWrappedCanvas().drawLine((float) startX, (float) startY, (float) stopX, (float) stopY, + paint); + } + + @Override + public void drawLines(double[] pts, int offset, int count, SafePaint paint) { + getWrappedCanvas().drawLines(this.toOffsetFloatAry(pts, sFloatAry), offset, count, paint); + } + + @Override + public void drawLines(double[] pts, SafePaint paint) { + getWrappedCanvas().drawLines(this.toOffsetFloatAry(pts, sFloatAry), paint); + } + + @Override + public void drawOval(Rect oval, SafePaint paint) { + getWrappedCanvas().drawOval(this.toOffsetRectF(oval, sRectF), paint); + } + + @Override + public void drawPaint(SafePaint paint) { + getWrappedCanvas().drawPaint(paint); + } + + @Override + public void drawPath(SafeTranslatedPath path, SafePaint paint) { + getWrappedCanvas().drawPath(path, paint); + } + + @Override + public void drawPicture(Picture picture, Rect dst) { + dst.offset(xOffset, yOffset); + getWrappedCanvas().drawPicture(picture, dst); + dst.offset(-xOffset, -yOffset); + } + + @Override + public void drawPicture(Picture picture) { + getWrappedCanvas().drawPicture(picture); + } + + @Override + public void drawPoint(double x, double y, SafePaint paint) { + x += xOffset; + y += yOffset; + getWrappedCanvas().drawPoint((float) x, (float) y, paint); + } + + @Override + public void drawPoints(double[] pts, int offset, int count, SafePaint paint) { + getWrappedCanvas().drawPoints(this.toOffsetFloatAry(pts, sFloatAry), offset, count, paint); + } + + @Override + public void drawPoints(double[] pts, SafePaint paint) { + getWrappedCanvas().drawPoints(this.toOffsetFloatAry(pts, sFloatAry), paint); + } + + @Override + public void drawPosText(char[] text, int index, int count, double[] pos, SafePaint paint) { + getWrappedCanvas().drawPosText(text, index, count, this.toOffsetFloatAry(pos, sFloatAry), + paint); + } + + @Override + public void drawPosText(String text, double[] pos, SafePaint paint) { + getWrappedCanvas().drawPosText(text, this.toOffsetFloatAry(pos, sFloatAry), paint); + } + + @Override + public void drawRGB(int r, int g, int b) { + getWrappedCanvas().drawRGB(r, g, b); + } + + @Override + public void drawRect(double left, double top, double right, double bottom, SafePaint paint) { + left += xOffset; + right += xOffset; + top += yOffset; + bottom += yOffset; + getWrappedCanvas() + .drawRect((float) left, (float) top, (float) right, (float) bottom, paint); + } + + @Override + public void drawRect(Rect r, SafePaint paint) { + r.offset(xOffset, yOffset); + getWrappedCanvas().drawRect(r, paint); + r.offset(-xOffset, -yOffset); + } + + @Override + public void drawRoundRect(Rect rect, float rx, float ry, SafePaint paint) { + getWrappedCanvas().drawRoundRect(this.toOffsetRectF(rect, sRectF), rx, ry, paint); + } + + @Override + public void drawText(String text, double x, double y, SafePaint paint) { + getWrappedCanvas().drawText(text, (float) (x + xOffset), (float) (y + yOffset), paint); + } + + @Override + public void drawText(char[] text, int index, int count, double x, double y, SafePaint paint) { + getWrappedCanvas().drawText(text, index, count, (float) (x + xOffset), + (float) (y + yOffset), paint); + } + + @Override + public void drawText(CharSequence text, int start, int end, double x, double y, SafePaint paint) { + getWrappedCanvas().drawText(text, start, end, (float) (x + xOffset), (float) (y + yOffset), + paint); + } + + @Override + public void drawText(String text, int start, int end, double x, double y, SafePaint paint) { + getWrappedCanvas().drawText(text, start, end, (float) (x + xOffset), (float) (y + yOffset), + paint); + } + + @Override + public void drawTextOnPath(char[] text, int index, int count, SafeTranslatedPath path, + float hOffset, float vOffset, SafePaint paint) { + getWrappedCanvas().drawTextOnPath(text, index, count, path, hOffset, vOffset, paint); + } + + @Override + public void drawTextOnPath(String text, SafeTranslatedPath path, float hOffset, float vOffset, + SafePaint paint) { + getWrappedCanvas().drawTextOnPath(text, path, hOffset, vOffset, paint); + } + + @Override + public void drawVertices(VertexMode mode, int vertexCount, double[] verts, int vertOffset, + float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, + int indexOffset, int indexCount, SafePaint paint) { + getWrappedCanvas().drawVertices(mode, vertexCount, this.toOffsetFloatAry(verts, sFloatAry), + vertOffset, texs, texOffset, colors, colorOffset, indices, indexOffset, indexCount, + paint); + } + + @Override + public boolean getClipBounds(Rect bounds) { + boolean success = getWrappedCanvas().getClipBounds(bounds); + if (bounds != null) + bounds.offset(-xOffset, -yOffset); + return success; + } + + @Override + public int getDensity() { + + return getWrappedCanvas().getDensity(); + } + + @Override + public DrawFilter getDrawFilter() { + + return getWrappedCanvas().getDrawFilter(); + } + + @Override + public int getHeight() { + + return getWrappedCanvas().getHeight(); + } + + @Override + public void getMatrix(Matrix ctm) { + + getWrappedCanvas().getMatrix(ctm); + } + + @Override + public int getSaveCount() { + + return getWrappedCanvas().getSaveCount(); + } + + @Override + public int getWidth() { + + return getWrappedCanvas().getWidth(); + } + + @Override + public boolean isOpaque() { + + return getWrappedCanvas().isOpaque(); + } + + @Override + public boolean quickReject(double left, double top, double right, double bottom, EdgeType type) { + left += xOffset; + right += xOffset; + top += yOffset; + bottom += yOffset; + return getWrappedCanvas().quickReject((float) left, (float) top, (float) right, + (float) bottom, type); + } + + @Override + public boolean quickReject(SafeTranslatedPath path, EdgeType type) { + return getWrappedCanvas().quickReject(path, type); + } + + @Override + public boolean quickReject(Rect rect, EdgeType type) { + + return getWrappedCanvas().quickReject(this.toOffsetRectF(rect, sRectF), type); + } + + @Override + public void restore() { + + getWrappedCanvas().restore(); + } + + @Override + public void restoreToCount(int saveCount) { + + getWrappedCanvas().restoreToCount(saveCount); + } + + @Override + public void rotate(float degrees) { + getWrappedCanvas().translate(this.xOffset, this.yOffset); + getWrappedCanvas().rotate(degrees); + getWrappedCanvas().translate(-this.xOffset, -this.yOffset); + } + + @Override + public void rotate(float degrees, double px, double py) { + getWrappedCanvas().rotate(degrees, (float) (px + xOffset), (float) (py + yOffset)); + } + + @Override + public int save() { + + return getWrappedCanvas().save(); + } + + @Override + public int save(int saveFlags) { + + return getWrappedCanvas().save(saveFlags); + } + + @Override + public int saveLayer(double left, double top, double right, double bottom, SafePaint paint, + int saveFlags) { + return getWrappedCanvas().saveLayer((float) (left + xOffset), (float) (top + yOffset), + (float) (right + xOffset), (float) (bottom + yOffset), paint, saveFlags); + } + + @Override + public int saveLayer(Rect bounds, SafePaint paint, int saveFlags) { + int result = getWrappedCanvas().saveLayer(this.toOffsetRectF(bounds, sRectF), paint, + saveFlags); + return result; + } + + @Override + public int saveLayerAlpha(double left, double top, double right, double bottom, int alpha, + int saveFlags) { + return getWrappedCanvas().saveLayerAlpha((float) (left + xOffset), (float) (top + yOffset), + (float) (right + xOffset), (float) (bottom + yOffset), alpha, saveFlags); + } + + @Override + public int saveLayerAlpha(Rect bounds, int alpha, int saveFlags) { + return getWrappedCanvas().saveLayerAlpha(this.toOffsetRectF(bounds, sRectF), alpha, + saveFlags); + } + + @Override + public void scale(float sx, float sy) { + getWrappedCanvas().scale(sx, sy); + } + + @Override + public void scale(float sx, float sy, double px, double py) { + getWrappedCanvas().scale(sx, sy, (float) (px + xOffset), (float) (py + yOffset)); + } + + @Override + public void setBitmap(Bitmap bitmap) { + getWrappedCanvas().setBitmap(bitmap); + } + + @Override + public void setDensity(int density) { + getWrappedCanvas().setDensity(density); + } + + @Override + public void setDrawFilter(DrawFilter filter) { + getWrappedCanvas().setDrawFilter(filter); + } + + @Override + public void setMatrix(Matrix matrix) { + getWrappedCanvas().setMatrix(matrix); + } + + @Override + public void skew(float sx, float sy) { + getWrappedCanvas().skew(sx, sy); + } + + @Override + public void translate(float dx, float dy) { + getWrappedCanvas().translate(dx, dy); + } + + @Override + protected Object clone() throws CloneNotSupportedException { + SafeTranslatedCanvas c = new SafeTranslatedCanvas(); + c.setCanvas(mCanvas); + return c; + } + + @Override + public boolean equals(Object o) { + return getWrappedCanvas().equals(o); + } + + @Override + public int hashCode() { + return getWrappedCanvas().hashCode(); + } + + @Override + public String toString() { + return getWrappedCanvas().toString(); + } + + /** + * Helper function to convert a Rect to RectF and adjust the values of the Rect by the offsets. + */ + protected final RectF toOffsetRectF(Rect rect, RectF reuse) { + if (reuse == null) + reuse = new RectF(); + + reuse.set(rect.left + xOffset, rect.top + yOffset, rect.right + xOffset, rect.bottom + + yOffset); + return reuse; + } + + /** + * Helper function to convert a Rect to RectF and adjust the values of the Rect by the offsets. + */ + protected final float[] toOffsetFloatAry(double[] rect, float[] reuse) { + if (reuse == null || reuse.length < rect.length) + reuse = new float[rect.length]; + + for (int a = 0; a < rect.length; a++) { + reuse[a] = (float) (rect[a] + (a % 2 == 0 ? xOffset : yOffset)); + } + return reuse; + } +} diff --git a/src/main/java/org/osmdroid/views/safecanvas/SafeTranslatedPath.java b/src/main/java/org/osmdroid/views/safecanvas/SafeTranslatedPath.java new file mode 100644 index 000000000..2d95c7720 --- /dev/null +++ b/src/main/java/org/osmdroid/views/safecanvas/SafeTranslatedPath.java @@ -0,0 +1,466 @@ +package org.osmdroid.views.safecanvas; + +import org.osmdroid.views.overlay.Overlay; + +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; + +/** + * The SafeTranslatedPath class is designed to work in conjunction with {@link SafeTranslatedCanvas} + * to work around various Android issues with large canvases. For the two classes to work together, + * call {@link #onDrawCycleStart} at the start of the {@link Overlay#drawSafe} method of your + * {@link Overlay}. This will set the adjustment needed to draw your Path safely on the canvas + * without any drawing distortion at high zoom levels. Methods of the {@link Path} class that use + * unsafe float types have been deprecated in favor of replacement methods that use doubles. + * + * @see {@link ISafeCanvas} + * + * @author Marc Kurtz + * + */ +public class SafeTranslatedPath extends Path { + + private final static RectF sRectF = new RectF(); + + public int xOffset = 0; + public int yOffset = 0; + + /** + * This method must be called at the start of the {@link Overlay#drawSafe} draw cycle + * method. This will adjust the Path to the current state of the {@link ISafeCanvas} passed to + * it. + */ + public void onDrawCycleStart(ISafeCanvas canvas) { + // Adjust the current position of the path + int deltaX = canvas.getXOffset() - xOffset; + int deltaY = canvas.getYOffset() - yOffset; + super.offset(deltaX, deltaY); + + // Record the new offset + xOffset = canvas.getXOffset(); + yOffset = canvas.getYOffset(); + } + + @Override + public void reset() { + super.reset(); + } + + @Override + public void rewind() { + super.rewind(); + } + + @Override + public void set(Path src) { + super.set(src); + } + + @Override + public FillType getFillType() { + return super.getFillType(); + } + + @Override + public void setFillType(FillType ft) { + super.setFillType(ft); + } + + @Override + public boolean isInverseFillType() { + return super.isInverseFillType(); + } + + @Override + public void toggleInverseFillType() { + super.toggleInverseFillType(); + } + + @Override + public boolean isEmpty() { + return super.isEmpty(); + } + + /** + * @deprecated Use {@link #isRect(Rect)} instead. + */ + @Override + public boolean isRect(RectF rect) { + // Should we offset here? + rect.offset(xOffset, yOffset); + boolean result = super.isRect(rect); + rect.offset(-xOffset, -yOffset); + return result; + } + + /** + * @see {@link #isRect(RectF)} + */ + public boolean isRect(Rect rect) { + // Should we offset here? + rect.offset(xOffset, yOffset); + boolean result = super.isRect(this.toOffsetRectF(rect, sRectF)); + rect.offset(-xOffset, -yOffset); + return result; + } + + /** + * @deprecated Use {@link #computeBounds(Rect, boolean)} instead. + */ + @Override + public void computeBounds(RectF bounds, boolean exact) { + super.computeBounds(bounds, exact); + bounds.offset(-xOffset, -yOffset); + } + + /** + * @see {@link #computeBounds(RectF, boolean)} + */ + public void computeBounds(Rect bounds, boolean exact) { + super.computeBounds(sRectF, exact); + bounds.set((int) sRectF.left, (int) sRectF.top, (int) sRectF.right, (int) sRectF.bottom); + bounds.offset(-xOffset, -yOffset); + } + + @Override + public void incReserve(int extraPtCount) { + super.incReserve(extraPtCount); + } + + /** + * @deprecated Use {@link #moveTo(double, double)} instead. + */ + @Override + public void moveTo(float x, float y) { + super.moveTo(x + xOffset, y + yOffset); + } + + /** + * @see {@link #moveTo(float, float)} + */ + public void moveTo(double x, double y) { + super.moveTo((float) (x + xOffset), (float) (y + yOffset)); + } + + @Override + public void rMoveTo(float dx, float dy) { + super.rMoveTo(dx, dy); + } + + /** + * @deprecated Use {@link #lineTo(double, double)} instead. + */ + @Override + public void lineTo(float x, float y) { + super.lineTo(x + xOffset, y + yOffset); + } + + /** + * @see {@link #lineTo(float, float)} + */ + public void lineTo(double x, double y) { + super.lineTo((float) (x + xOffset), (float) (y + yOffset)); + } + + @Override + public void rLineTo(float dx, float dy) { + super.rLineTo(dx, dy); + } + + /** + * @deprecated Use {@link #quadTo(double, double, double, double)} instead. + */ + @Override + public void quadTo(float x1, float y1, float x2, float y2) { + super.quadTo(x1 + xOffset, y1 + yOffset, x2 + xOffset, y2 + yOffset); + } + + /** + * @see {@link #quadTo(float, float, float, float)} + */ + public void quadTo(double x1, double y1, double x2, double y2) { + super.quadTo((float) (x1 + xOffset), (float) (y1 + yOffset), (float) (x2 + xOffset), + (float) (y2 + yOffset)); + } + + @Override + public void rQuadTo(float dx1, float dy1, float dx2, float dy2) { + super.rQuadTo(dx1, dy1, dx2, dy2); + } + + /** + * @deprecated Use {@link #cubicTo(double, double, double, double, double, double)} instead. + */ + @Override + public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { + super.cubicTo(x1 + xOffset, y1 + yOffset, x2 + xOffset, y2 + yOffset, x3 + xOffset, y3 + + yOffset); + } + + /** + * @see {@link #cubicTo(float, float, float, float, float, float)} + */ + public void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + super.cubicTo((float) (x1 + xOffset), (float) (y1 + yOffset), (float) (x2 + xOffset), + (float) (y2 + yOffset), (float) (x3 + xOffset), (float) (y3 + yOffset)); + } + + @Override + public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { + super.rCubicTo(x1, y1, x2, y2, x3, y3); + } + + /** + * @deprecated use {@link #arcTo(Rect, float, float, boolean)} + */ + @Override + public void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) { + oval.offset(xOffset, yOffset); + super.arcTo(oval, startAngle, sweepAngle, forceMoveTo); + oval.offset(-xOffset, -yOffset); + } + + /** + * @see {@link #arcTo(RectF, float, float, boolean)} + */ + public void arcTo(Rect oval, float startAngle, float sweepAngle, boolean forceMoveTo) { + oval.offset(xOffset, yOffset); + super.arcTo(this.toOffsetRectF(oval, sRectF), startAngle, sweepAngle, forceMoveTo); + oval.offset(-xOffset, -yOffset); + } + + /** + * @deprecated use {@link #arcTo(Rect, float, float)} + */ + @Override + public void arcTo(RectF oval, float startAngle, float sweepAngle) { + oval.offset(xOffset, yOffset); + super.arcTo(oval, startAngle, sweepAngle); + oval.offset(-xOffset, -yOffset); + } + + /** + * @see {@link #arcTo(RectF, float, float)} + */ + public void arcTo(Rect oval, float startAngle, float sweepAngle) { + oval.offset(xOffset, yOffset); + super.arcTo(this.toOffsetRectF(oval, sRectF), startAngle, sweepAngle); + oval.offset(-xOffset, -yOffset); + } + + @Override + public void close() { + super.close(); + } + + /** + * @deprecated use {@link #addRect(Rect, Direction)} + */ + @Override + public void addRect(RectF rect, Direction dir) { + rect.offset(xOffset, yOffset); + super.addRect(rect, dir); + rect.offset(-xOffset, -yOffset); + } + + /** + * @see {@link #addRect(RectF, Direction)} + */ + public void addRect(Rect rect, Direction dir) { + rect.offset(xOffset, yOffset); + super.addRect(this.toOffsetRectF(rect, sRectF), dir); + rect.offset(-xOffset, -yOffset); + } + + /** + * @deprecated use {@link #addRect(double, double, double, double, Direction)} + */ + @Override + public void addRect(float left, float top, float right, float bottom, Direction dir) { + super.addRect(left + xOffset, top + yOffset, right + xOffset, bottom + yOffset, dir); + } + + /** + * @see {@link #addRect(float, float, float, float, Direction)} + */ + public void addRect(double left, double top, double right, double bottom, Direction dir) { + super.addRect((float) (left + xOffset), (float) (top + yOffset), (float) (right + xOffset), + (float) (bottom + yOffset), dir); + } + + /** + * @deprecated use {@link #addOval(Rect, Direction) + */ + @Override + public void addOval(RectF oval, Direction dir) { + oval.offset(xOffset, yOffset); + super.addOval(oval, dir); + oval.offset(-xOffset, -yOffset); + } + + /** + * @see {@link #addOval(RectF, Direction) + */ + public void addOval(Rect oval, Direction dir) { + oval.offset(xOffset, yOffset); + super.addOval(this.toOffsetRectF(oval, sRectF), dir); + oval.offset(-xOffset, -yOffset); + } + + /** + * @deprecated use {@link #addCircle(double, double, double, Direction)} + */ + @Override + public void addCircle(float x, float y, float radius, Direction dir) { + super.addCircle(x + xOffset, y + yOffset, radius, dir); + } + + /** + * @see {@link #addCircle(float, float, float, Direction)} + */ + public void addCircle(double x, double y, float radius, Direction dir) { + super.addCircle((float) (x + xOffset), (float) (y + yOffset), radius, dir); + } + + /** + * @deprecated use {@link #addArc(Rect, float, float)} + */ + @Override + public void addArc(RectF oval, float startAngle, float sweepAngle) { + oval.offset(xOffset, yOffset); + super.addArc(oval, startAngle, sweepAngle); + oval.offset(-xOffset, -yOffset); + } + + /** + * @see {@link #addArc(RectF, float, float)} + */ + public void addArc(Rect oval, float startAngle, float sweepAngle) { + oval.offset(xOffset, yOffset); + super.addArc(this.toOffsetRectF(oval, sRectF), startAngle, sweepAngle); + oval.offset(-xOffset, -yOffset); + } + + /** + * @deprecated use {@link #addRoundRect(Rect, float, float)} + */ + @Override + public void addRoundRect(RectF rect, float rx, float ry, Direction dir) { + rect.offset(xOffset, yOffset); + super.addRoundRect(rect, rx, ry, dir); + rect.offset(-xOffset, -yOffset); + } + + /** + * @see {@link #addRoundRect(RectF, float, float)} + */ + public void addRoundRect(Rect rect, float rx, float ry, Direction dir) { + rect.offset(xOffset, yOffset); + super.addRoundRect(this.toOffsetRectF(rect, sRectF), rx, ry, dir); + rect.offset(-xOffset, -yOffset); + } + + /** + * @deprecated use {@link #addRoundRect(Rect, float, Direction)} + */ + public void addRoundRect(RectF rect, float[] radii, Direction dir) { + rect.offset(xOffset, yOffset); + super.addRoundRect(rect, radii, dir); + rect.offset(-xOffset, -yOffset); + } + + /** + * @see {@link #addRoundRect(RectF, float, Direction)} + */ + public void addRoundRect(Rect rect, float[] radii, Direction dir) { + rect.offset(xOffset, yOffset); + super.addRoundRect(this.toOffsetRectF(rect, sRectF), radii, dir); + rect.offset(-xOffset, -yOffset); + } + + @Override + public void addPath(Path src, float dx, float dy) { + boolean safePath = src instanceof SafeTranslatedPath; + if (!safePath) + src.offset(xOffset, yOffset); + super.addPath(src, dx, dy); + if (!safePath) + src.offset(-xOffset, -yOffset); + } + + @Override + public void addPath(Path src) { + boolean safePath = src instanceof SafeTranslatedPath; + if (!safePath) + src.offset(xOffset, yOffset); + super.addPath(src); + if (!safePath) + src.offset(-xOffset, -yOffset); + } + + @Override + public void addPath(Path src, Matrix matrix) { + boolean safePath = src instanceof SafeTranslatedPath; + if (!safePath) + matrix.preTranslate(xOffset, yOffset); + super.addPath(src, matrix); + if (!safePath) + matrix.preTranslate(-xOffset, -yOffset); + } + + @Override + public void offset(float dx, float dy, Path dst) { + super.offset(dx, dy, dst); + } + + @Override + public void offset(float dx, float dy) { + super.offset(dx, dy); + } + + /** + * @deprecated use {@link #setLastPoint(double, double)} + */ + @Override + public void setLastPoint(float dx, float dy) { + super.setLastPoint(dx + xOffset, dy + yOffset); + } + + /** + * @see {@link #setLastPoint(float, float)} + */ + public void setLastPoint(double dx, double dy) { + super.setLastPoint((float) (dx + xOffset), (float) (dy + yOffset)); + } + + @Override + public void transform(Matrix matrix, Path dst) { + // Should we offset here? + super.transform(matrix, dst); + } + + @Override + public void transform(Matrix matrix) { + // Should we offset here? + super.transform(matrix); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + } + + /** + * Helper function to convert a Rect to RectF and adjust the values of the Rect by the offsets. + */ + protected final RectF toOffsetRectF(Rect rect, RectF reuse) { + if (reuse == null) + reuse = new RectF(); + + reuse.set(rect.left + xOffset, rect.top + yOffset, rect.right + xOffset, rect.bottom + + yOffset); + return reuse; + } +} diff --git a/src/main/java/org/osmdroid/views/util/Mercator.java b/src/main/java/org/osmdroid/views/util/Mercator.java new file mode 100644 index 000000000..d61aff966 --- /dev/null +++ b/src/main/java/org/osmdroid/views/util/Mercator.java @@ -0,0 +1,159 @@ +// Created by plusminus on 17:53:07 - 25.09.2008 +package org.osmdroid.views.util; + +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.util.BoundingBoxE6; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.util.constants.MapViewConstants; + +import android.graphics.Point; + +/** + * http://wiki.openstreetmap.org/index.php/Mercator + * http://developers.cloudmade.com/projects/tiles/examples/convert-coordinates-to-tile-numbers + * + * @author Nicolas Gramlich + * + * @deprecated Use TileSystem instead + * + */ +public class Mercator implements MapViewConstants { + // =========================================================== + // Constants + // =========================================================== + + final static double DEG2RAD = Math.PI / 180; + + // =========================================================== + // Fields + // =========================================================== + + // =========================================================== + // Constructors + // =========================================================== + + /** + * This is a utility class with only static members. + */ + private Mercator() { + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + /** + * Mercator projection of GeoPoint at given zoom level + * + * @param aLat + * latitude in degrees [-89000000 to 89000000] + * @param aLon + * longitude in degrees [-180000000 to 180000000] + * @param zoom + * zoom level + * @param aUseAsReturnValue + * @return Point with x,y in the range [-2^(zoom-1) to 2^(zoom-1)] + */ + public static Point projectGeoPoint(final int aLatE6, final int aLonE6, final int aZoom, + final Point reuse) { + return projectGeoPoint(aLatE6 * 1E-6, aLonE6 * 1E-6, aZoom, reuse); + } + + /** + * Mercator projection of GeoPoint at given zoom level + * + * @param pGeoPoint + * @param zoom + * zoom level + * @param pUseAsReturnValue + * @return Point with x,y in the range [-2^(zoom-1) to 2^(zoom-1)] + */ + public static Point projectGeoPoint(final IGeoPoint pGeoPoint, final int pZoom, + final Point pUseAsReturnValue) { + return projectGeoPoint(pGeoPoint.getLatitudeE6() * 1E-6, pGeoPoint.getLongitudeE6() * 1E-6, + pZoom, pUseAsReturnValue); + } + + /** + * Mercator projection of GeoPoint at given zoom level + * + * @param aLat + * latitude in degrees [-89 to 89] + * @param aLon + * longitude in degrees [-180 to 180] + * @param zoom + * zoom level + * @param aUseAsReturnValue + * @return Point with x,y in the range [-2^(zoom-1) to 2^(zoom-1)] + */ + public static Point projectGeoPoint(final double aLat, final double aLon, final int aZoom, + final Point aUseAsReturnValue) { + final Point p = aUseAsReturnValue != null ? aUseAsReturnValue : new Point(0, 0); + + p.x = ((int) Math.floor((aLon + 180) / 360 * (1 << aZoom))); + p.y = ((int) Math.floor((1 - Math.log(Math.tan(aLat * DEG2RAD) + 1 + / Math.cos(aLat * DEG2RAD)) + / Math.PI) + / 2 * (1 << aZoom))); + + return p; + } + + /** + * Get bounding box from reverse Mercator projection. + * + * @param left + * @param top + * @param right + * @param bottom + * @param zoom + * @return + */ + public static BoundingBoxE6 getBoundingBoxFromCoords(final int left, final int top, + final int right, final int bottom, final int zoom) { + return new BoundingBoxE6(tile2lat(top, zoom), tile2lon(right, zoom), + tile2lat(bottom, zoom), tile2lon(left, zoom)); + } + + /** + * Get bounding box from reverse Mercator projection. + * + * @param aMapTile + * @param aZoom + * @return + */ + public static BoundingBoxE6 getBoundingBoxFromPointInMapTile(final Point aMapTile, + final int aZoom) { + return new BoundingBoxE6(tile2lat(aMapTile.y, aZoom), tile2lon(aMapTile.x + 1, aZoom), + tile2lat(aMapTile.y + 1, aZoom), tile2lon(aMapTile.x, aZoom)); + } + + /** + * Reverse Mercator projection of Point at given zoom level + * + */ + public static GeoPoint projectPoint(final int x, final int y, final int aZoom) { + return new GeoPoint((int) (tile2lat(y, aZoom) * 1E6), (int) (tile2lon(x, aZoom) * 1E6)); + } + + public static double tile2lon(final int x, final int aZoom) { + return (double) x / (1 << aZoom) * 360.0 - 180; + } + + public static double tile2lat(final int y, final int aZoom) { + final double n = Math.PI - 2.0 * Math.PI * y / (1 << aZoom); + return 180.0 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/views/util/MyMath.java b/src/main/java/org/osmdroid/views/util/MyMath.java new file mode 100644 index 000000000..bdc0db0d7 --- /dev/null +++ b/src/main/java/org/osmdroid/views/util/MyMath.java @@ -0,0 +1,73 @@ +// Created by plusminus on 20:36:01 - 26.09.2008 +package org.osmdroid.views.util; + +/** + * + * @author Nicolas Gramlich + * + */ +public class MyMath { + // =========================================================== + // Constants + // =========================================================== + + // =========================================================== + // Fields + // =========================================================== + + // =========================================================== + // Constructors + // =========================================================== + + /** + * This is a utility class with only static members. + */ + private MyMath() { + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + // =========================================================== + // Methods from SuperClass/Interfaces + // =========================================================== + + // =========================================================== + // Methods + // =========================================================== + + /** + * Calculates i.e. the increase of zoomlevel needed when the visible latitude needs to be bigger + * by factor. + * + * Assert.assertEquals(1, getNextSquareNumberAbove(1.1f)); Assert.assertEquals(2, + * getNextSquareNumberAbove(2.1f)); Assert.assertEquals(2, getNextSquareNumberAbove(3.9f)); + * Assert.assertEquals(3, getNextSquareNumberAbove(4.1f)); Assert.assertEquals(3, + * getNextSquareNumberAbove(7.9f)); Assert.assertEquals(4, getNextSquareNumberAbove(8.1f)); + * Assert.assertEquals(5, getNextSquareNumberAbove(16.1f)); + * + * Assert.assertEquals(-1, - getNextSquareNumberAbove(1 / 0.4f) + 1); Assert.assertEquals(-2, - + * getNextSquareNumberAbove(1 / 0.24f) + 1); + * + * @param factor + * @return + */ + public static int getNextSquareNumberAbove(final float factor) { + int out = 0; + int cur = 1; + int i = 1; + while (true) { + if (cur > factor) + return out; + + out = i; + cur *= 2; + i++; + } + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/views/util/PathProjection.java b/src/main/java/org/osmdroid/views/util/PathProjection.java new file mode 100644 index 000000000..e2eee73dd --- /dev/null +++ b/src/main/java/org/osmdroid/views/util/PathProjection.java @@ -0,0 +1,96 @@ +package org.osmdroid.views.util; + +import java.util.List; + +import org.osmdroid.util.BoundingBoxE6; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.util.TileSystem; +import org.osmdroid.views.MapView.Projection; + +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; + +public class PathProjection { + + public static Path toPixels(Projection projection, final List in, + final Path reuse) { + return toPixels(projection, in, reuse, true); + } + + public static Path toPixels(Projection projection, final List in, + final Path reuse, final boolean doGudermann) throws IllegalArgumentException { + if (in.size() < 2) { + throw new IllegalArgumentException("List of GeoPoints needs to be at least 2."); + } + + final Path out = (reuse != null) ? reuse : new Path(); + out.incReserve(in.size()); + + boolean first = true; + for (final GeoPoint gp : in) { + final Point underGeopointTileCoords = TileSystem.LatLongToPixelXY( + gp.getLatitudeE6() / 1E6, gp.getLongitudeE6() / 1E6, projection.getZoomLevel(), + null); + TileSystem.PixelXYToTileXY(underGeopointTileCoords.x, underGeopointTileCoords.y, + underGeopointTileCoords); + + /* + * Calculate the Latitude/Longitude on the left-upper ScreenCoords of the MapTile. + */ + final Point upperRight = TileSystem.TileXYToPixelXY(underGeopointTileCoords.x, + underGeopointTileCoords.y, null); + final Point lowerLeft = TileSystem.TileXYToPixelXY(underGeopointTileCoords.x + + TileSystem.getTileSize(), + underGeopointTileCoords.y + TileSystem.getTileSize(), null); + final GeoPoint neGeoPoint = TileSystem.PixelXYToLatLong(upperRight.x, upperRight.y, + projection.getZoomLevel(), null); + final GeoPoint swGeoPoint = TileSystem.PixelXYToLatLong(lowerLeft.x, lowerLeft.y, + projection.getZoomLevel(), null); + final BoundingBoxE6 bb = new BoundingBoxE6(neGeoPoint.getLatitudeE6(), + neGeoPoint.getLongitudeE6(), swGeoPoint.getLatitudeE6(), + swGeoPoint.getLongitudeE6()); + + final PointF relativePositionInCenterMapTile; + if (doGudermann && (projection.getZoomLevel() < 7)) { + relativePositionInCenterMapTile = bb + .getRelativePositionOfGeoPointInBoundingBoxWithExactGudermannInterpolation( + gp.getLatitudeE6(), gp.getLongitudeE6(), null); + } else { + relativePositionInCenterMapTile = bb + .getRelativePositionOfGeoPointInBoundingBoxWithLinearInterpolation( + gp.getLatitudeE6(), gp.getLongitudeE6(), null); + } + + final Rect screenRect = projection.getScreenRect(); + Point centerMapTileCoords = TileSystem.PixelXYToTileXY(screenRect.centerX(), + screenRect.centerY(), null); + final Point upperLeftCornerOfCenterMapTile = TileSystem.TileXYToPixelXY( + centerMapTileCoords.x, centerMapTileCoords.y, null); + final int tileDiffX = centerMapTileCoords.x - underGeopointTileCoords.x; + final int tileDiffY = centerMapTileCoords.y - underGeopointTileCoords.y; + final int underGeopointTileScreenLeft = upperLeftCornerOfCenterMapTile.x + - (TileSystem.getTileSize() * tileDiffX); + final int underGeopointTileScreenTop = upperLeftCornerOfCenterMapTile.y + - (TileSystem.getTileSize() * tileDiffY); + + final int x = underGeopointTileScreenLeft + + (int) (relativePositionInCenterMapTile.x * TileSystem.getTileSize()); + final int y = underGeopointTileScreenTop + + (int) (relativePositionInCenterMapTile.y * TileSystem.getTileSize()); + + /* Add up the offset caused by touch. */ + if (first) { + out.moveTo(x, y); + // out.moveTo(x + MapView.this.mTouchMapOffsetX, y + + // MapView.this.mTouchMapOffsetY); + } else { + out.lineTo(x, y); + } + first = false; + } + + return out; + } +} diff --git a/src/main/java/org/osmdroid/views/util/constants/MapViewConstants.java b/src/main/java/org/osmdroid/views/util/constants/MapViewConstants.java new file mode 100644 index 000000000..a310c3d85 --- /dev/null +++ b/src/main/java/org/osmdroid/views/util/constants/MapViewConstants.java @@ -0,0 +1,36 @@ +// Created by plusminus on 18:00:24 - 25.09.2008 +package org.osmdroid.views.util.constants; + +/** + * + * This class contains constants used by the map view. + * + * @author Nicolas Gramlich + * + */ +public interface MapViewConstants { + // =========================================================== + // Final Fields + // =========================================================== + + public static final boolean DEBUGMODE = false; + + public static final int NOT_SET = Integer.MIN_VALUE; + + public static final int ANIMATION_SMOOTHNESS_LOW = 4; + public static final int ANIMATION_SMOOTHNESS_DEFAULT = 10; + public static final int ANIMATION_SMOOTHNESS_HIGH = 20; + + public static final int ANIMATION_DURATION_SHORT = 500; + public static final int ANIMATION_DURATION_DEFAULT = 1000; + public static final int ANIMATION_DURATION_LONG = 2000; + + /** Minimum Zoom Level */ + public static final int MINIMUM_ZOOMLEVEL = 0; + + /** + * Maximum Zoom Level - we use Integers to store zoom levels so overflow happens at 2^32 - 1, + * but we also have a tile size that is typically 2^8, so (32-1)-8-1 = 22 + */ + public static final int MAXIMUM_ZOOMLEVEL = 22; +} diff --git a/src/main/java/org/osmdroid/views/util/constants/MathConstants.java b/src/main/java/org/osmdroid/views/util/constants/MathConstants.java new file mode 100644 index 000000000..d03587aa7 --- /dev/null +++ b/src/main/java/org/osmdroid/views/util/constants/MathConstants.java @@ -0,0 +1,24 @@ +// Created by plusminus on 17:27:54 - 30.09.2008 +package org.osmdroid.views.util.constants; + +/** + * + * @author Nicolas Gramlich + * + */ +public interface MathConstants { + // =========================================================== + // Final Fields + // =========================================================== + + public static final float DEG2RAD = (float) (Math.PI / 180.0); + public static final float RAD2DEG = (float) (180.0 / Math.PI); + + public static final float PI = (float) Math.PI; + public static final float PI_2 = PI / 2.0f; + public static final float PI_4 = PI / 4.0f; + + // =========================================================== + // Methods + // =========================================================== +} diff --git a/src/main/java/org/osmdroid/views/util/constants/OverlayConstants.java b/src/main/java/org/osmdroid/views/util/constants/OverlayConstants.java new file mode 100644 index 000000000..979653c6d --- /dev/null +++ b/src/main/java/org/osmdroid/views/util/constants/OverlayConstants.java @@ -0,0 +1,16 @@ +package org.osmdroid.views.util.constants; + +/** + * This class contains constants used by the overlays. + */ +public interface OverlayConstants { + // =========================================================== + // Final Fields + // =========================================================== + + public static final boolean DEBUGMODE = false; + + public static final int NOT_SET = Integer.MIN_VALUE; + + public static final int DEFAULT_ZOOMLEVEL_MINIMAP_DIFFERENCE = 3; +} diff --git a/src/main/java/slf4j-api-1.7.5.jar b/src/main/java/slf4j-api-1.7.5.jar new file mode 100644 index 0000000000000000000000000000000000000000..8766455d8756ebdac09d36eea2c08db90c18b124 GIT binary patch literal 26084 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 literal 0 HcmV?d00001 diff --git a/src/main/main.iml b/src/main/main.iml new file mode 100644 index 000000000..d68d09a26 --- /dev/null +++ b/src/main/main.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/org/osmdroid/center.png b/src/main/resources/org/osmdroid/center.png new file mode 100644 index 0000000000000000000000000000000000000000..025bdad41a473ed2cec5b61a51949ae1141cfb81 GIT binary patch literal 2335 zcmV+)3E=jLP))48pqf1MTd4!kFH~&G07OIp z2ms?ufmiXv1c4M6numiM3;i)6snC@WgbR3N5MS<5Vy``R z^mWFVFa6Ag)YL03ym%b$;u@4b}QsU&LHlx3>G8Z5{8K5eT>hBViykQ6=8rcA~BH zveHA-e^g+u+H|FoTN#?|38*X1(fSS%J7)nWA|DYcrx$Ahb!pwNtGwkk(=mPeblKF@ zWIS^J=8xOnZtqlOh78Igd8BQkD~DHnkdYrS7$?b9h82IjTvc^mMnol)3J-t~5vk_| zNGA+cuH@?hQ~>Z1(F|{fCwI>BQgr;OA1}T15_DY`wY9a8H4A_9)+eo>9UDB9IC0mq z-|f1lZfV5o(9Sp@Qwa6<&Q(2rbt!-Vfcf)&&YuR5dv+z1vkZm+6kL1#vZk8(;q|XH ze||St)V!LtR|ksyiO9f36_6Na_6N_KisSgaA@TR0hD8u0}a0k9WznOL7Vd3SWNy_y69ZXKB6h z-I}qk@eZMB+8Kz<%uHv&f(20kV2sJSx;kvxvIR<9hpkV*HXJ_M8x3V&#H;UWy6J|B zS#$V6xL*Lc6o3jq05E9HqctZ({%k*I3OI2-W9BTsnqjjHZ!Y?ehn*ce-+nh^qHN!N zjSof|8XCUW*?N0>p=la!x$S1Ecp#{R_mAa{^c`Qmu5oR3eog^Rm}6kB2jQWU!V#8q zg@e(^u;AA;Rz0p3#Qgqj?hWL6GoN%M!21HZFv&Qmn?_yIsC+_E}Sw4;q&@o zQ4A)cutXeE0ZH`=2N+zTu1RN-y)UKxK9PpZm(-20T<4}`X;=^5s55_wSw-pu^ z8k}m$V^@=v&|}zC*}&rkU~k1 zQF^)!%kF;W@~s7>#hEAD)X~?T`eehz0CxsDMzTxp%%Mvj$-?MI{aCST8FKP+@ZyUv z(#XgN0Gy#AM$((%u?sKB`nqj%a@)}14>B4yEO;UXl-rHOD2AQjapb3ZuxYJHq0 zd>#KdDy)Qa;&k$~u9Q+*03v56{|>+;;aY~<`uWS&%)Gg&_LxzntLtO{D<7Pxk+fBk230NWy#(~^qjX2dAoG2*H&Yatz+yKCHz7gy@ z`cpZ;oO3X__<=v)@b~-*-4_!>=!>^Q(>bVbmihep{fz+j0~kiCg(p3jOsPy#N|%UE z0f+!lC4^%-2BctcEdiSoR6#CM4%Z7n{9lh|X$VO~5`b`AN0^Qc+chC=4okXVu5^(y zGdb1d7X)xtBqH%K!#kc6bOl2&Q32bvU>c?YAaS8${{xpT7sb9JKWG2|002ovPDHLk FV1marPAmWb literal 0 HcmV?d00001 diff --git a/src/main/resources/org/osmdroid/direction_arrow.png b/src/main/resources/org/osmdroid/direction_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..e69291f8e0e684b41c88c3532b9ae62f9b08d7d8 GIT binary patch literal 2642 zcmV-Y3a#~tP)GU_Mpsmct}8IakRX6IF(^o&bRZ}TZ7C&` zHgTIDj`PEI{4;*fcYnNR$8qd*Y+*-w^0n`|=X~C|zs|iTAp{mn><+mSz<6(WEV@`W z*hyVFO`<5V)CuWj^KNQeHU42i@PG(R5pkku)j* z7ZKirK+p4Chx>kTz(@COGDW7%olIbt0G6fJeHT84jq5k#)31&pJZ?m>ykf2+z!fJP zFy;sMKfC9(@S*}bolfSa!EGesPXJ_1tM)|VzO#PAsdabX@*pmaoyVIW9`&xuz5O!) zHUOn_)x{-2Z0{0Lb)>7SD}FiOmmPzh&aD&j4Fem3~_$1Szj z#=@!>)+Ni4XUZ$+n|^6;8LGIKzhDG+KG{*nRlEgY zrT&v2B%W4;Vc!c~FN6TV?vQ1p!R+4tv&RchO&mjQu?+)*1In?_-*~%-rEZ@`^TcGO8KJ`q5X{?iE#Y2*H`LzhzZ><;-(Tg~;a zu4})Ef7pK%6$Q1ZFS-tg-gruxQKA*S-Ldgh{Z8k0s|w{!0BZn%fJud;JsV`kF79-0 zwQv=0CEy5n#b61pONRphB0SXEa4ik_z5xJ?_`U;^1y)tB6#(u_*WcfD=#s$_Tt~nw z03d)OuHvn1-90aYPN$Q(aiNDmw51aUIqXGEZgnuu+0MmVZ&?_-jmIkE<2lRu8H}gZbX%=?*Z4;8JJ8)EGw-u1GsBp zlPNw^SdxH3;AXw(DZ{ek+057-vMJY?cYl3#vpnSQ2ROhuz&L?1{o&f9hzMN%uh3k- zh7n=ULU_7QPa4ZCAkAhceV5t9@2Xqcn!}B8Ohtyk7(kpr93TnWrw*(#1?ML}!<9>G zDBo1nYIn$MvKiHV_NB3CfO>*iuKzZ)(3>(w54SX|<(}|`WTZGFaIQa+_7E3{#e?w9 zxG}AcqN#3;0>Z-!*-r6Yo=AEb0PJ-BvJ`*}00J1!*4plnTgwZpuP!Ssr_ta!FfPD2 zKwN;>Z1eip!CYk;@=>t z4-+#3ur;;`B6OzP*L~X3SW=c5!h3qLbPTTRQXAVHa$Cg}%bUuUl+ak{JU~K&<3Kp> zLqPR_aWN-ib3paS75EYjwX3*6&b`I%kmnq_bf2YZEGd&jAZ7PZQ+5>qga=z1)+zzv zfr79;YXyR;2O%-3O%iDqF1?(9^+e7iZVaJuxlIMIV{Qyne5SA@0iW+W+(*gK(v}_0 z9l3h+3e1N5&2=@6(nMqcpg4#L_?1yagl`T6mok*~iNy59{9@oFY)xxr#%Y(`A?Ey;vbpG_f^pt-sR7SndqGv0;D-Ovl`qL2yp@rH{FJbxZ>t z4xL6}aXzfYEAj!{2>`mUiZq7#fX4jben4o+ouJ1$o!hO9Q)kPn)$&Bt1p$GeGJ$a1 zudRWMlG~ajOURK7Fy@#cOL7vDxUQ$`&xjD*(QmQ3rHv88Bc0CeR{f+0KwQ!oW<^T< z^6Z}1!n>ZhcdvE^hQ+I1e1B*Jjw2NFK@hBoyUA?0qRp?4zH8JYSM%Hg11ULPob!Kzq3N#?qH(k~w28B4&aE=@ z67A3=s*j}eWIPvlI=5P)Lj8O|eZQ{bj)xG*8AmQJK+rdXGhcrd>mNKVO$LWg#Z|lw z;2!`^1DNXVjwOxR9kLl(R&qTtY|J+mu3u@rT3X%G#*rUDIPQfpQjWuaeGV&48~SsI z+V*$78H3SMq2VB(eI)cXKHnFnGM&Ay7xdlK*z>E>JJLDV8 zM$3ncxClo4e{MA2wnQf5Eh#D^+8KiPlwsNNYzeN@>15gtp>ELExr#)c3gvHS0eyae;0mL0+81U?NcJvW29M=<0 zJu9h1^7<_`dC{)lD#VH-2cHSY?GE`>BK!$JD}Z@`i*|u|Ir%{^Ff&Q|6)?0TgRED_ z0Q~>2fEP3bqC}d0ufO Ap8x;= literal 0 HcmV?d00001 diff --git a/src/main/resources/org/osmdroid/ic_menu_compass.png b/src/main/resources/org/osmdroid/ic_menu_compass.png new file mode 100644 index 0000000000000000000000000000000000000000..7717dde51db12424875f4b99e6476c3dfbf1b738 GIT binary patch literal 3943 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 BhI9Y` literal 0 HcmV?d00001 diff --git a/src/main/resources/org/osmdroid/ic_menu_mapmode.png b/src/main/resources/org/osmdroid/ic_menu_mapmode.png new file mode 100644 index 0000000000000000000000000000000000000000..d85cab5d6b1d14a2e490d917712761ffd6832447 GIT binary patch literal 1923 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 A@c;k- literal 0 HcmV?d00001 diff --git a/src/main/resources/org/osmdroid/next.png b/src/main/resources/org/osmdroid/next.png new file mode 100644 index 0000000000000000000000000000000000000000..30560f269e911f25939428b7a2a8edcbde322ba9 GIT binary patch literal 1670 zcmV;126_33P)7?P@{Ht^!d& zDGM%(%U$l>yLaZC^Yp{a+`W5oi!P1cbySe`_}ebT7$MDclI}Y;S&RJYKeW!>5E6PW=b~R_voT)T037^)t_7QQOB8411q;m zM=r{(zhW=Rg|w(TZ?P@{l=H>Yl=1~O^ln}F`@K)^>e-avvj6eu$&UpPR&w_&@4n-y z8?Rf6Ac&-q8kIc*p}A>3-dmiH@px1{h4LtqQGxYqHni-0b=PA(oAWpSqcQH&ZR%$O zxGV)rx^AMmr2%QMmb zGIJdOH2EqJ2vH4ToE9lw>WR7K~e}uoQP*3F)Guo6Pto0{hW_^ zZ+w`M(ouO}-NtJ_)84grRe$*HTD{A5La$1kiW)I^F&GhoAiV~>7_v;LDk@6Fmbf%E zPBFSj97jYpLNp*J^B66@%Z14i?p^)h!hylR?C9ywE!+QCy!}G~6ravM!bp;T!T>}t z7*xQBLGeu42})6cQZ!EFDyRqbibqia=VPL{3_ziLp0kA$^xU?-b$)Z}rk*YN8}|Ps z`rc&#*2R?V1m$P~V?x4QNZQhw+lBT2adTzUJ zEdvL47e>cNHXeA~J~N{TMZ|bVP#sZ7gbYYPRonzZ5K00T%8W39B(OChMp)c&Gu@qE zW%u8oA3JmMZ12G*-18!0st%;5n?PL+kVya%h%{;kHv%Tz3Q`Beflu#^CXRG>tzur( z?*26J?3uGerEebpD<@N36%n7MzyN{zX#gYX>@$#B@h}09>}>@V@KU|SXuub{*75#- z#w&XVcMcufW4|-@78ijaeZ>PV1CW|vNyn{J{O1TlGoAY|S&AkgfiXhKjd5=BII4*Q zxMW#2nr2pX^>XO&;nI;e4!wBz7j|={!c?7(0A@xJs`nE|-g>RrI)6b$K$68iz58a* z1Z6>ITdsTgP0i<{w^3DmD&E#`E!}N*^U}cIipPgfK6z+|{Z+OuYA0DuwW=Z_VValR zXK&{k7ZbXO&^tnJ$$3Ypp3r+jJfSMVs=ltBz4w2;?Tw3jlC^J@_S_N{=f1?Q-7geQ z{Cjlso4fqWK%9oHYhxhF9B^qJxOWHnAcINU`UrpkLB%B1?;YIGw4CPhVtw|7XU5*% zSKM%FpFft0*>wE+@T!^=p{lBgxH^lnR%^m%1&g3behf?7zCz)&#?QU@yHiL1TzPo( zC}XKeEr6|y7-}{mRdpg#|B>L+8E5;O$pA#rKy%#M(#!Dc@01T5-1n~|zlr;b7bvIi zXMJiNus{UF*)d>d5vqC-iE0K2(#XVv)ENV4Yp{qtFa5DNH2mgo|M~Oy$2Bf#W^GjWpv+`X;+YhlwU+ z7O*KyRU9y-sp2bR?@#>nwWsZ1)}>C|lN8!)693zQkMhg@jIL^oPy6%yzZJ?mZFz~a Q1^@s607*qoM6N<$f*_~ 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 + + + + + + + + + + +