From d029a3f90857a44f8b91c9331289e97ed931521d Mon Sep 17 00:00:00 2001 From: m1ga Date: Sat, 15 Oct 2022 22:13:54 +0200 Subject: [PATCH 1/6] feat(android): mbtiles overlay --- android/manifest | 2 +- .../src/ti/map/MapBoxOfflineTileProvider.java | 220 ++++++++++++++++++ android/src/ti/map/MapModule.java | 2 + android/src/ti/map/ViewProxy.java | 39 ++++ 4 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 android/src/ti/map/MapBoxOfflineTileProvider.java diff --git a/android/manifest b/android/manifest index 1380c543..675925fe 100644 --- a/android/manifest +++ b/android/manifest @@ -2,7 +2,7 @@ # this is your module manifest and used by Titanium # during compilation, packaging, distribution, etc. # -version: 5.5.0 +version: 5.6.0 apiversion: 4 architectures: arm64-v8a armeabi-v7a x86 x86_64 description: External version of Map module using native Google Maps library diff --git a/android/src/ti/map/MapBoxOfflineTileProvider.java b/android/src/ti/map/MapBoxOfflineTileProvider.java new file mode 100644 index 00000000..1b609c28 --- /dev/null +++ b/android/src/ti/map/MapBoxOfflineTileProvider.java @@ -0,0 +1,220 @@ +package ti.map; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.Tile; +import com.google.android.gms.maps.model.TileProvider; +import java.io.Closeable; +import java.io.File; + +public class MapBoxOfflineTileProvider implements TileProvider, Closeable +{ + + // ------------------------------------------------------------------------ + // Instance Variables + // ------------------------------------------------------------------------ + + private int mMinimumZoom = Integer.MIN_VALUE; + + private int mMaximumZoom = Integer.MAX_VALUE; + + private LatLngBounds mBounds; + + private SQLiteDatabase mDatabase; + + private final Object mDatabaseLock = new Object(); + + // ------------------------------------------------------------------------ + // Constructors + // ------------------------------------------------------------------------ + + public MapBoxOfflineTileProvider(File file) + { + this(file.getAbsolutePath()); + } + + public MapBoxOfflineTileProvider(String pathToFile) + { + int flags = SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS; + this.mDatabase = SQLiteDatabase.openDatabase(pathToFile, null, flags); + this.calculateZoomConstraints(); + this.calculateBounds(); + } + + // ------------------------------------------------------------------------ + // TileProvider Interface + // ------------------------------------------------------------------------ + + @Override + public Tile getTile(int x, int y, int z) + { + Tile tile = NO_TILE; + synchronized (mDatabaseLock) { + if (this.isZoomLevelAvailable(z) && this.isDatabaseAvailable()) { + String[] projection = { "tile_data" }; + int row = ((int) (Math.pow(2, z) - y) - 1); + String predicate = "tile_row = ? AND tile_column = ? AND zoom_level = ?"; + String[] values = { String.valueOf(row), String.valueOf(x), String.valueOf(z) }; + Cursor c = this.mDatabase.query("tiles", projection, predicate, values, null, null, null); + if (c != null) { + c.moveToFirst(); + if (!c.isAfterLast()) { + tile = new Tile(256, 256, c.getBlob(0)); + } + c.close(); + } + } + } + return tile; + } + + // ------------------------------------------------------------------------ + // Closeable Interface + // ------------------------------------------------------------------------ + + /** + * Closes the provider, cleaning up any background resources. + * + *

+ * You must call {@link #close()} when you are finished using an instance of + * this provider. Failing to do so may leak resources, such as the backing + * SQLiteDatabase. + *

+ */ + @Override + public void close() + { + synchronized (mDatabaseLock) { + if (this.mDatabase != null) { + this.mDatabase.close(); + this.mDatabase = null; + } + } + } + + // ------------------------------------------------------------------------ + // Public Methods + // ------------------------------------------------------------------------ + + public void swapDatabase(File newDatabaseFile) + { + synchronized (mDatabaseLock) { + mDatabase.close(); + int flags = SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS; + this.mDatabase = SQLiteDatabase.openDatabase(newDatabaseFile.getAbsolutePath(), null, flags); + } + } + /** + * The minimum zoom level supported by this provider. + * + * @return the minimum zoom level supported or {@link Integer.MIN_VALUE} if + * it could not be determined. + */ + public int getMinimumZoom() + { + return this.mMinimumZoom; + } + + /** + * The maximum zoom level supported by this provider. + * + * @return the maximum zoom level supported or {@link Integer.MAX_VALUE} if + * it could not be determined. + */ + public int getMaximumZoom() + { + return this.mMaximumZoom; + } + + /** + * The geographic bounds available from this provider. + * + * @return the geographic bounds available or {@link null} if it could not + * be determined. + */ + public LatLngBounds getBounds() + { + return this.mBounds; + } + + /** + * Determines if the requested zoom level is supported by this provider. + * + * @param zoom The requested zoom level. + * @return {@code true} if the requested zoom level is supported by this + * provider. + */ + public boolean isZoomLevelAvailable(int zoom) + { + return (zoom >= this.mMinimumZoom) && (zoom <= this.mMaximumZoom); + } + + // ------------------------------------------------------------------------ + // Private Methods + // ------------------------------------------------------------------------ + + private void calculateZoomConstraints() + { + if (this.isDatabaseAvailable()) { + String[] projection = new String[] { "value" }; + + String[] minArgs = new String[] { "minzoom" }; + + String[] maxArgs = new String[] { "maxzoom" }; + + Cursor c; + + c = this.mDatabase.query("metadata", projection, "name = ?", minArgs, null, null, null); + + c.moveToFirst(); + if (!c.isAfterLast()) { + this.mMinimumZoom = c.getInt(0); + } + c.close(); + + c = this.mDatabase.query("metadata", projection, "name = ?", maxArgs, null, null, null); + + c.moveToFirst(); + if (!c.isAfterLast()) { + this.mMaximumZoom = c.getInt(0); + } + c.close(); + } + } + + private void calculateBounds() + { + if (this.isDatabaseAvailable()) { + String[] projection = new String[] { "value" }; + + String[] subArgs = new String[] { "bounds" }; + + Cursor c = this.mDatabase.query("metadata", projection, "name = ?", subArgs, null, null, null); + + c.moveToFirst(); + if (!c.isAfterLast()) { + String[] parts = c.getString(0).split(",\\s*"); + + double w = Double.parseDouble(parts[0]); + double s = Double.parseDouble(parts[1]); + double e = Double.parseDouble(parts[2]); + double n = Double.parseDouble(parts[3]); + + LatLng ne = new LatLng(n, e); + LatLng sw = new LatLng(s, w); + + this.mBounds = new LatLngBounds(sw, ne); + } + c.close(); + } + } + + private boolean isDatabaseAvailable() + { + synchronized (mDatabaseLock) { + return (this.mDatabase != null) && (this.mDatabase.isOpen()); + } + } +} diff --git a/android/src/ti/map/MapModule.java b/android/src/ti/map/MapModule.java index ff21d374..770c1cd3 100644 --- a/android/src/ti/map/MapModule.java +++ b/android/src/ti/map/MapModule.java @@ -78,6 +78,8 @@ public class MapModule extends KrollModule implements OnMapsSdkInitializedCallba public static final String PROPERTY_HEADING = "heading"; public static final String PROPERTY_PITCH = "pitch"; + @Kroll.constant + public static final int TYPE_NONE = GoogleMap.MAP_TYPE_NONE; @Kroll.constant public static final int NORMAL_TYPE = GoogleMap.MAP_TYPE_NORMAL; @Kroll.constant diff --git a/android/src/ti/map/ViewProxy.java b/android/src/ti/map/ViewProxy.java index a09b9faf..23db2b0a 100644 --- a/android/src/ti/map/ViewProxy.java +++ b/android/src/ti/map/ViewProxy.java @@ -15,6 +15,7 @@ import com.google.android.gms.maps.model.TileOverlay; import com.google.android.gms.maps.model.TileOverlayOptions; import com.google.maps.android.heatmaps.HeatmapTileProvider; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -27,11 +28,15 @@ import org.appcelerator.kroll.common.Log; import org.appcelerator.kroll.common.TiMessenger; import org.appcelerator.titanium.TiApplication; +import org.appcelerator.titanium.TiBlob; import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.TiFileProxy; +import org.appcelerator.titanium.io.TiBaseFile; import org.appcelerator.titanium.proxy.TiViewProxy; import org.appcelerator.titanium.util.TiConvert; import org.appcelerator.titanium.view.TiUIView; import ti.map.AnnotationProxy.AnnotationDelegate; +import ti.modules.titanium.filesystem.FileProxy; @Kroll. proxy(creatableInModule = MapModule.class, @@ -83,6 +88,7 @@ public class ViewProxy extends TiViewProxy implements AnnotationDelegate private static final int MSG_REMOVE_ALL_IMAGE_OVERLAYS = MSG_FIRST_ID + 933; private static final int MSG_ADD_HEAT_MAP = MSG_FIRST_ID + 941; + private static final int MSG_ADD_MBILES_MAP = MSG_FIRST_ID + 942; private final ArrayList preloadRoutes; private final ArrayList preloadPolygons; @@ -337,6 +343,12 @@ public boolean handleMessage(Message msg) result.setResult(null); return true; } + case MSG_ADD_MBILES_MAP: { + result = ((AsyncResult) msg.obj); + handleAddMbtileMap(result.getArg()); + result.setResult(null); + return true; + } case MSG_CONTAINS_COORDINATE: { result = ((AsyncResult) msg.obj); @@ -672,6 +684,33 @@ public void addHeatMap(Object coordinates) } } + @Kroll.method + public void addMbtileMap(Object data) + { + if (TiApplication.isUIThread()) { + handleAddMbtileMap(data); + } else { + TiMessenger.sendBlockingMainMessage(getMainHandler().obtainMessage(MSG_ADD_MBILES_MAP), data); + } + } + + public void handleAddMbtileMap(Object data) + { + if (data instanceof TiFileProxy) { + TiBaseFile file = ((TiFileProxy) data).getBaseFile(); + MapBoxOfflineTileProvider mbOfflineTileProvider = new MapBoxOfflineTileProvider(file.getNativeFile()); + TileOverlayOptions tileOverlayOptions = new TileOverlayOptions().tileProvider(mbOfflineTileProvider); + + TiUIView view = peekView(); + GoogleMap map = (view instanceof TiUIMapView) ? ((TiUIMapView) view).getMap() : null; + if (map != null) { + map.addTileOverlay(tileOverlayOptions); + } else { + this.preloadTileOverlayOptionsList.add(tileOverlayOptions); + } + } + } + public void handleAddHeatMap(Object coordinates) { // Validate. From 5804bf67e5561e52726111b60b873caf22519dc7 Mon Sep 17 00:00:00 2001 From: m1ga Date: Sun, 16 Oct 2022 11:26:10 +0200 Subject: [PATCH 2/6] error check --- android/src/ti/map/MapBoxOfflineTileProvider.java | 11 ++++++++--- android/src/ti/map/ViewProxy.java | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/android/src/ti/map/MapBoxOfflineTileProvider.java b/android/src/ti/map/MapBoxOfflineTileProvider.java index 1b609c28..74a8c42a 100644 --- a/android/src/ti/map/MapBoxOfflineTileProvider.java +++ b/android/src/ti/map/MapBoxOfflineTileProvider.java @@ -8,6 +8,7 @@ import com.google.android.gms.maps.model.TileProvider; import java.io.Closeable; import java.io.File; +import org.appcelerator.kroll.common.Log; public class MapBoxOfflineTileProvider implements TileProvider, Closeable { @@ -38,9 +39,13 @@ public MapBoxOfflineTileProvider(File file) public MapBoxOfflineTileProvider(String pathToFile) { int flags = SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS; - this.mDatabase = SQLiteDatabase.openDatabase(pathToFile, null, flags); - this.calculateZoomConstraints(); - this.calculateBounds(); + try { + this.mDatabase = SQLiteDatabase.openDatabase(pathToFile, null, flags); + this.calculateZoomConstraints(); + this.calculateBounds(); + } catch (Error e) { + Log.e("TiUIMapView", "Error loading mbtiles file"); + } } // ------------------------------------------------------------------------ diff --git a/android/src/ti/map/ViewProxy.java b/android/src/ti/map/ViewProxy.java index 23db2b0a..4ab8058c 100644 --- a/android/src/ti/map/ViewProxy.java +++ b/android/src/ti/map/ViewProxy.java @@ -698,6 +698,10 @@ public void handleAddMbtileMap(Object data) { if (data instanceof TiFileProxy) { TiBaseFile file = ((TiFileProxy) data).getBaseFile(); + if (!file.exists()) { + Log.e(TAG, "mbtiles not found"); + return; + } MapBoxOfflineTileProvider mbOfflineTileProvider = new MapBoxOfflineTileProvider(file.getNativeFile()); TileOverlayOptions tileOverlayOptions = new TileOverlayOptions().tileProvider(mbOfflineTileProvider); From d188a15782d8ec68370e65f151432f56bfe6d506 Mon Sep 17 00:00:00 2001 From: m1ga Date: Sun, 16 Oct 2022 12:26:21 +0200 Subject: [PATCH 3/6] max/minZoomLevel --- android/src/ti/map/TiUIMapView.java | 15 ++++++++++++++- android/src/ti/map/ViewProxy.java | 6 ------ apidoc/View.yml | 11 ++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/android/src/ti/map/TiUIMapView.java b/android/src/ti/map/TiUIMapView.java index f52b68c5..c48de1b6 100644 --- a/android/src/ti/map/TiUIMapView.java +++ b/android/src/ti/map/TiUIMapView.java @@ -88,6 +88,8 @@ public class TiUIMapView extends TiUIFragment private DefaultClusterRenderer clusterRender; private MarkerManager mMarkerManager; private MarkerManager.Collection collection; + private float minZoom = -1; + private float maxZoom = -1; public TiUIMapView(final TiViewProxy proxy, Activity activity) { @@ -239,7 +241,12 @@ public void onMapReady(GoogleMap gMap) markerCollection.setInfoWindowAdapter(this); markerCollection.setOnInfoWindowClickListener(this); markerCollection.setOnMarkerDragListener(this); - + if (minZoom != -1) { + map.setMinZoomPreference(minZoom); + } + if (maxZoom != -1) { + map.setMaxZoomPreference(maxZoom); + } ((ViewProxy) proxy).clearPreloadObjects(); } @@ -322,6 +329,12 @@ public void processMapProperties(KrollDict d) if (clusterRender != null) clusterRender.setMinClusterSize(d.getInt(MapModule.PROPERTY_MIN_CLUSTER_SIZE)); } + if (d.containsKey("maxZoomLevel")) { + maxZoom = Float.parseFloat(d.getString("maxZoomLevel")); + } + if (d.containsKey("minZoomLevel")) { + minZoom = Float.parseFloat(d.getString("minZoomLevel")); + } } @Override diff --git a/android/src/ti/map/ViewProxy.java b/android/src/ti/map/ViewProxy.java index 4ab8058c..6825960a 100644 --- a/android/src/ti/map/ViewProxy.java +++ b/android/src/ti/map/ViewProxy.java @@ -791,11 +791,8 @@ public float getMinZoom() } } - // clang-format off - @Kroll.method @Kroll.getProperty public float getMaxZoomLevel() - // clang-format on { if (TiApplication.isUIThread()) { return getMaxZoom(); @@ -804,11 +801,8 @@ public float getMaxZoomLevel() } } - // clang-format off - @Kroll.method @Kroll.getProperty public float getMinZoomLevel() - // clang-format on { if (TiApplication.isUIThread()) { return getMinZoom(); diff --git a/apidoc/View.yml b/apidoc/View.yml index 04109548..9670f3bb 100644 --- a/apidoc/View.yml +++ b/apidoc/View.yml @@ -709,9 +709,9 @@ properties: platforms: [android] - name: maxZoomLevel - summary: Returns the maximum zoom level available at the current camera position. + summary: Maximum zoom level available at the current camera position. description: | - Returns the maximum zoom level for the current camera position. + Set and returns the maximum zoom level for the current camera position. This takes into account what map type is currently being used. For example, satellite or terrain may have a lower max zoom level than the base map tiles. @@ -720,18 +720,19 @@ properties: type: Number platforms: [android] permission: read-only + availability: creation - name: minZoomLevel - summary: Returns the minimum zoom level available at the current camera position. + summary: Minimum zoom level available at the current camera position. description: | - Returns the minimum zoom level. This is the same for every location (unlike the maximum zoom level) + Set and returns the minimum zoom level. This is the same for every location (unlike the maximum zoom level) but may vary between devices and map sizes. This will only give the correct value after the 'complete' event is fired. since: "3.2.3" type: Number platforms: [android] - permission: read-only + availability: creation - name: minClusterSize summary: Sets the minium size of a cluster. From 92f3b6970994701fd49130d21df8a559d9e9e8a3 Mon Sep 17 00:00:00 2001 From: m1ga Date: Sun, 16 Oct 2022 21:46:33 +0200 Subject: [PATCH 4/6] zindex --- android/src/ti/map/ViewProxy.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/android/src/ti/map/ViewProxy.java b/android/src/ti/map/ViewProxy.java index 6825960a..5f3d2441 100644 --- a/android/src/ti/map/ViewProxy.java +++ b/android/src/ti/map/ViewProxy.java @@ -696,13 +696,17 @@ public void addMbtileMap(Object data) public void handleAddMbtileMap(Object data) { - if (data instanceof TiFileProxy) { - TiBaseFile file = ((TiFileProxy) data).getBaseFile(); - if (!file.exists()) { + HashMap hm = (HashMap) data; + Object file = hm.get("file"); + int layerIndex = TiConvert.toInt(hm.get("zIndex"), -1); + + if (file instanceof TiFileProxy) { + TiBaseFile baseFile = ((TiFileProxy) file).getBaseFile(); + if (!baseFile.exists()) { Log.e(TAG, "mbtiles not found"); return; } - MapBoxOfflineTileProvider mbOfflineTileProvider = new MapBoxOfflineTileProvider(file.getNativeFile()); + MapBoxOfflineTileProvider mbOfflineTileProvider = new MapBoxOfflineTileProvider(baseFile.getNativeFile()); TileOverlayOptions tileOverlayOptions = new TileOverlayOptions().tileProvider(mbOfflineTileProvider); TiUIView view = peekView(); @@ -710,7 +714,11 @@ public void handleAddMbtileMap(Object data) if (map != null) { map.addTileOverlay(tileOverlayOptions); } else { - this.preloadTileOverlayOptionsList.add(tileOverlayOptions); + if (layerIndex != -1) { + this.preloadTileOverlayOptionsList.add(layerIndex, tileOverlayOptions); + } else { + this.preloadTileOverlayOptionsList.add(tileOverlayOptions); + } } } } From cb2d1f91f47b501296104ffeca8537198328866b Mon Sep 17 00:00:00 2001 From: m1ga Date: Sun, 16 Oct 2022 22:14:18 +0200 Subject: [PATCH 5/6] range check --- android/src/ti/map/ViewProxy.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/android/src/ti/map/ViewProxy.java b/android/src/ti/map/ViewProxy.java index 5f3d2441..9018c19d 100644 --- a/android/src/ti/map/ViewProxy.java +++ b/android/src/ti/map/ViewProxy.java @@ -714,7 +714,10 @@ public void handleAddMbtileMap(Object data) if (map != null) { map.addTileOverlay(tileOverlayOptions); } else { - if (layerIndex != -1) { + if (layerIndex > -1) { + if (layerIndex >= this.preloadTileOverlayOptionsList.size()) { + layerIndex = this.preloadTileOverlayOptionsList.size(); + } this.preloadTileOverlayOptionsList.add(layerIndex, tileOverlayOptions); } else { this.preloadTileOverlayOptionsList.add(tileOverlayOptions); From 4cafb0a244526c80e96e92f34026d7678e048295 Mon Sep 17 00:00:00 2001 From: m1ga Date: Sun, 16 Oct 2022 22:23:33 +0200 Subject: [PATCH 6/6] tileOverlaySize property --- android/src/ti/map/ViewProxy.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/android/src/ti/map/ViewProxy.java b/android/src/ti/map/ViewProxy.java index 9018c19d..62c698d2 100644 --- a/android/src/ti/map/ViewProxy.java +++ b/android/src/ti/map/ViewProxy.java @@ -664,6 +664,12 @@ public void handleDeselectAnnotation(Object annotation) } } + @Kroll.getProperty + public int getTileOverlaySize() + { + return this.preloadTileOverlayOptionsList.size(); + } + @Kroll.method public void addRoute(RouteProxy route) {