From 0d34f9f371409a5b1aba9d389c865d749f48c669 Mon Sep 17 00:00:00 2001 From: Bas Date: Tue, 19 Dec 2023 17:49:52 +0100 Subject: [PATCH] Custom tile overlay --- .../plugins/googlemaps/CapacitorGoogleMap.kt | 81 ++++++++++++++++++ .../googlemaps/CapacitorGoogleMapsPlugin.kt | 39 +++++++++ .../CapacitorGoogleMapsTileOverlay.kt | 29 +++++++ google-maps/e2e-tests/ios/.gitignore | 2 +- google-maps/src/definitions.ts | 12 +++ google-maps/src/implementation.ts | 10 +++ google-maps/src/map.ts | 12 +++ google-maps/src/web.ts | 84 ++++++++++++++++++- 8 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsTileOverlay.kt diff --git a/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMap.kt b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMap.kt index a22aa2208..500ab7c20 100644 --- a/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMap.kt +++ b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMap.kt @@ -19,6 +19,7 @@ import com.google.maps.android.clustering.ClusterManager import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import java.io.InputStream +import java.net.HttpURLConnection import java.net.URL @@ -173,6 +174,86 @@ class CapacitorGoogleMap( } } + fun addTileOverlay(tileOverlay: CapacitorGoogleMapsTileOverlay, callback: (result: Result) -> Unit) { + try { + googleMap ?: throw GoogleMapNotAvailable() + var tileOverlayId: String + + val bitmapFunc = CoroutineScope(Dispatchers.IO).async { + val url = URL(tileOverlay.imageSrc) + val connection: HttpURLConnection = url.openConnection() as HttpURLConnection + + connection.doInput = true + connection.connect() + + val input: InputStream = connection.inputStream + + BitmapFactory.decodeStream(input) + } + + CoroutineScope(Dispatchers.Main).launch { + /* + var tileProvider: TileProvider = object : UrlTileProvider(256, 256) { + override fun getTileUrl(x: Int, y: Int, zoom: Int): URL? { + + /* Define the URL pattern for the tile images */ + val url = "https://avatars.githubusercontent.com/u/103097039?v=4" + return if (!checkTileExists(x, y, zoom)) { + null + } else try { + URL(url) + } catch (e: MalformedURLException) { + throw AssertionError(e) + } + } + + /* + * Check that the tile server supports the requested x, y and zoom. + * Complete this stub according to the tile range you support. + * If you support a limited range of tiles at different zoom levels, then you + * need to define the supported x, y range at each zoom level. + */ + private fun checkTileExists(x: Int, y: Int, zoom: Int): Boolean { + val minZoom = 1 + val maxZoom = 16 + return zoom in minZoom..maxZoom + } + } + + Log.d("TileOverlay ^^^ ", "tileProvider") + + val tileOverlay = googleMap?.addTileOverlay( + TileOverlayOptions() + .tileProvider(tileProvider) + ) + */ + + val bitmap = bitmapFunc.await() + + // Now you can safely use the bitmap + if (bitmap != null) { + val imageDescriptor = BitmapDescriptorFactory.fromBitmap(bitmap) + + val groundOverlay = googleMap?.addGroundOverlay( + GroundOverlayOptions() + .image(imageDescriptor) + .positionFromBounds(tileOverlay.imageBounds) + .transparency(tileOverlay.opacity) + .zIndex(tileOverlay.zIndex) + .visible(tileOverlay.visible) + ) + + tileOverlay.googleMapsTileOverlay = groundOverlay + tileOverlayId = groundOverlay!!.id + + callback(Result.success(tileOverlayId)) + } + } + } catch (e: GoogleMapsError) { + callback(Result.failure(e)) + } + } + fun addMarkers( newMarkers: List, callback: (ids: Result>) -> Unit diff --git a/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsPlugin.kt b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsPlugin.kt index 143eca887..0ea2bcd6c 100644 --- a/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsPlugin.kt +++ b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsPlugin.kt @@ -210,6 +210,45 @@ class CapacitorGoogleMapsPlugin : Plugin(), OnMapsSdkInitializedCallback { } } + @PluginMethod + fun addTileOverlay(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val imageBoundsObj = call.getObject("imageBounds") ?: throw InvalidArgumentsError("imageBounds object is missing") + + val imageSrc = call.getString("imageSrc") + val opacity = call.getFloat("opacity", 1.0f) + val zIndex = call.getFloat("zIndex", 0.0f) + val visible = call.getBoolean("visible", true) + + val tileOverlayConfig = JSONObject() + tileOverlayConfig.put("imageBounds", imageBoundsObj) + tileOverlayConfig.put("imageSrc", imageSrc) + tileOverlayConfig.put("opacity", opacity) + tileOverlayConfig.put("zIndex", zIndex) + tileOverlayConfig.put("visible", visible) + + val map = maps[id] + map ?: throw MapNotFoundError() + + val tileOptions = CapacitorGoogleMapsTileOverlay(tileOverlayConfig) + + map.addTileOverlay(tileOptions) { result -> + val tileOverlayId = result.getOrThrow() + + val res = JSObject() + res.put("id", tileOverlayId) + call.resolve(res) + } + } catch (e: GoogleMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + @PluginMethod fun addMarker(call: PluginCall) { try { diff --git a/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsTileOverlay.kt b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsTileOverlay.kt new file mode 100644 index 000000000..b757dfcbf --- /dev/null +++ b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsTileOverlay.kt @@ -0,0 +1,29 @@ +package com.capacitorjs.plugins.googlemaps + +import com.google.android.gms.maps.model.GroundOverlay +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import org.json.JSONObject + +class CapacitorGoogleMapsTileOverlay(fromJSONObject: JSONObject) { + var imageBounds: LatLngBounds + var imageSrc: String? = null + var opacity: Float = 1.0f + var zIndex: Float = 0.0f + var visible: Boolean = true + var googleMapsTileOverlay: GroundOverlay? = null + + init { + val latLngObj = fromJSONObject.getJSONObject("imageBounds") + val north = latLngObj.optDouble("north", 0.0) + val south = latLngObj.optDouble("south", 0.0) + val east = latLngObj.optDouble("east", 0.0) + val west = latLngObj.optDouble("west", 0.0) + + imageBounds = LatLngBounds(LatLng(south, west), LatLng(north, east)) + imageSrc = fromJSONObject.optString("imageSrc", null) + zIndex = fromJSONObject.optLong("zIndex", 0).toFloat() + visible = fromJSONObject.optBoolean("visible", true) + opacity = 1.0f - fromJSONObject.optDouble("opacity", 1.0).toFloat() + } +} \ No newline at end of file diff --git a/google-maps/e2e-tests/ios/.gitignore b/google-maps/e2e-tests/ios/.gitignore index 01ad52029..75e8c5aec 100644 --- a/google-maps/e2e-tests/ios/.gitignore +++ b/google-maps/e2e-tests/ios/.gitignore @@ -1,9 +1,9 @@ App/build App/Pods +App/Podfile.lock App/App/public DerivedData xcuserdata # Cordova plugins for Capacitor capacitor-cordova-ios-plugins - diff --git a/google-maps/src/definitions.ts b/google-maps/src/definitions.ts index 6fa91254a..7bb3bc9d3 100644 --- a/google-maps/src/definitions.ts +++ b/google-maps/src/definitions.ts @@ -65,6 +65,18 @@ export interface Point { y: number; } +/** + * For web, all the javascript TileOverlay options are available as + * For iOS and Android only the config options declared on TileOverlay are available. + */ +export interface TileOverlay { + getTile: (x: number, y: number, zoom: number) => string; + opacity?: number; + visible?: boolean; + zIndex?: number; + debug?: boolean; +} + /** * For web, all the javascript Polygon options are available as * Polygon extends google.maps.PolygonOptions. diff --git a/google-maps/src/implementation.ts b/google-maps/src/implementation.ts index 76555ada0..66b711ecb 100644 --- a/google-maps/src/implementation.ts +++ b/google-maps/src/implementation.ts @@ -58,6 +58,15 @@ export interface DestroyMapArgs { id: string; } +export interface AddTileOverlayArgs { + id: string; + getTile: (x: number, y: number, zoom: number) => string; + opacity?: number; + debug?: boolean; + visible?: boolean; + zIndex?: number; +} + export interface RemoveMarkerArgs { id: string; markerId: string; @@ -173,6 +182,7 @@ export interface CapacitorGoogleMapsPlugin extends Plugin { create(options: CreateMapArgs): Promise; enableTouch(args: { id: string }): Promise; disableTouch(args: { id: string }): Promise; + addTileOverlay(args: AddTileOverlayArgs): Promise; addMarker(args: AddMarkerArgs): Promise<{ id: string }>; addMarkers(args: AddMarkersArgs): Promise<{ ids: string[] }>; removeMarker(args: RemoveMarkerArgs): Promise; diff --git a/google-maps/src/map.ts b/google-maps/src/map.ts index 67b6324e6..1a4ca01cb 100644 --- a/google-maps/src/map.ts +++ b/google-maps/src/map.ts @@ -19,6 +19,7 @@ import type { CircleClickCallbackData, Polyline, PolylineCallbackData, + TileOverlay, } from './definitions'; import { LatLngBounds, MapType } from './definitions'; import type { CreateMapArgs } from './implementation'; @@ -38,6 +39,7 @@ export interface GoogleMapInterface { minClusterSize?: number, ): Promise; disableClustering(): Promise; + addTileOverlay(tiles: TileOverlay): Promise; addMarker(marker: Marker): Promise; addMarkers(markers: Marker[]): Promise; removeMarker(id: string): Promise; @@ -371,6 +373,16 @@ export class GoogleMap { }); } + /** + * Adds a TileOverlay to the map + */ + async addTileOverlay(tiles: TileOverlay): Promise { + return await CapacitorGoogleMaps.addTileOverlay({ + id: this.id, + ...tiles, + }); + } + /** * Adds a marker to the map * diff --git a/google-maps/src/web.ts b/google-maps/src/web.ts index 5b82addec..4a9c2025d 100644 --- a/google-maps/src/web.ts +++ b/google-maps/src/web.ts @@ -11,6 +11,7 @@ import { import type { Marker } from './definitions'; import { MapType, LatLngBounds } from './definitions'; import type { + AddTileOverlayArgs, AddMarkerArgs, CameraArgs, AddMarkersArgs, @@ -35,6 +36,42 @@ import type { RemovePolylinesArgs, } from './implementation'; +class CoordMapType implements google.maps.MapType { + tileSize: google.maps.Size; + alt: string | null = null; + maxZoom = 17; + minZoom = 0; + name: string | null = null; + projection: google.maps.Projection | null = null; + radius = 6378137; + + constructor(tileSize: google.maps.Size) { + this.tileSize = tileSize; + } + getTile( + coord: google.maps.Point, + zoom: number, + ownerDocument: Document, + ): HTMLElement { + const div = ownerDocument.createElement('div'); + const pElement = ownerDocument.createElement('p'); + pElement.innerHTML = `x = ${coord.x}, y = ${coord.y}, zoom = ${zoom}`; + pElement.style.color = 'rgba(0, 0, 0, 0.5)'; + pElement.style.padding = '0 20px'; + div.appendChild(pElement); + + div.style.width = this.tileSize.width + 'px'; + div.style.height = this.tileSize.height + 'px'; + div.style.fontSize = '10'; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px'; + div.style.borderColor = 'rgba(0, 0, 0, 0.5)'; + return div; + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + releaseTile(): void {} +} + export class CapacitorGoogleMapsWeb extends WebPlugin implements CapacitorGoogleMapsPlugin @@ -132,7 +169,6 @@ export class CapacitorGoogleMapsWeb }); const google = await loader.load(); this.gMapsRef = google.maps; - console.log('Loaded google maps API'); } } @@ -260,6 +296,50 @@ export class CapacitorGoogleMapsWeb map.fitBounds(bounds, _args.padding); } + async addTileOverlay(_args: AddTileOverlayArgs): Promise { + const map = this.maps[_args.id].map; + + const tileSize = new google.maps.Size(256, 256); // Create a google.maps.Size instance + const coordMapType = new CoordMapType(tileSize); + + // Create a TileOverlay object + const customMapOverlay = new google.maps.ImageMapType({ + getTileUrl: function (coord, zoom) { + return _args.getTile(coord.x, coord.y, zoom); + }, + tileSize: new google.maps.Size(256, 256), + opacity: _args?.opacity, + name: 'tileoverlay', + }); + + // Draw Tiles + map.overlayMapTypes.insertAt(0, coordMapType); // insert coordMapType at the first position + + // Add the TileOverlay to the map + map.overlayMapTypes.push(customMapOverlay); + + // Optionally, you can set debug mode if needed + if (_args?.debug) { + map.addListener('mousemove', function (event: any) { + console.log('Mouse Coordinates: ', event.latLng.toString()); + }); + } + + // Set visibility based on the 'visible' property + if (!_args?.visible) { + map.overlayMapTypes.pop(); // Remove the last overlay (customMapOverlay) from the stack + } + + // Set zIndex based on the 'zIndex' property + if (_args?.zIndex !== undefined) { + // Move the customMapOverlay to the specified index in the overlay stack + map.overlayMapTypes.setAt( + map.overlayMapTypes.getLength() - 1, + customMapOverlay, + ); + } + } + async addMarkers(_args: AddMarkersArgs): Promise<{ ids: string[] }> { const markerIds: string[] = []; const map = this.maps[_args.id]; @@ -434,7 +514,6 @@ export class CapacitorGoogleMapsWeb } async create(_args: CreateMapArgs): Promise { - console.log(`Create map: ${_args.id}`); await this.importGoogleLib(_args.apiKey, _args.region, _args.language); this.maps[_args.id] = { @@ -449,7 +528,6 @@ export class CapacitorGoogleMapsWeb } async destroy(_args: DestroyMapArgs): Promise { - console.log(`Destroy map: ${_args.id}`); const mapItem = this.maps[_args.id]; mapItem.element.innerHTML = ''; mapItem.map.unbindAll();