Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(@capacitor/google-maps): Custom tile overlay #1970

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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


Expand Down Expand Up @@ -173,6 +174,86 @@ class CapacitorGoogleMap(
}
}

fun addTileOverlay(tileOverlay: CapacitorGoogleMapsTileOverlay, callback: (result: Result<String>) -> 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<CapacitorGoogleMapMarker>,
callback: (ids: Result<List<String>>) -> Unit
Expand Down
Expand Up @@ -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 {
Expand Down
@@ -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()
}
}
2 changes: 1 addition & 1 deletion 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

12 changes: 12 additions & 0 deletions google-maps/src/definitions.ts
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions google-maps/src/implementation.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -173,6 +182,7 @@ export interface CapacitorGoogleMapsPlugin extends Plugin {
create(options: CreateMapArgs): Promise<void>;
enableTouch(args: { id: string }): Promise<void>;
disableTouch(args: { id: string }): Promise<void>;
addTileOverlay(args: AddTileOverlayArgs): Promise<void>;
addMarker(args: AddMarkerArgs): Promise<{ id: string }>;
addMarkers(args: AddMarkersArgs): Promise<{ ids: string[] }>;
removeMarker(args: RemoveMarkerArgs): Promise<void>;
Expand Down
12 changes: 12 additions & 0 deletions google-maps/src/map.ts
Expand Up @@ -19,6 +19,7 @@ import type {
CircleClickCallbackData,
Polyline,
PolylineCallbackData,
TileOverlay,
} from './definitions';
import { LatLngBounds, MapType } from './definitions';
import type { CreateMapArgs } from './implementation';
Expand All @@ -38,6 +39,7 @@ export interface GoogleMapInterface {
minClusterSize?: number,
): Promise<void>;
disableClustering(): Promise<void>;
addTileOverlay(tiles: TileOverlay): Promise<void>;
addMarker(marker: Marker): Promise<string>;
addMarkers(markers: Marker[]): Promise<string[]>;
removeMarker(id: string): Promise<void>;
Expand Down Expand Up @@ -371,6 +373,16 @@ export class GoogleMap {
});
}

/**
* Adds a TileOverlay to the map
*/
async addTileOverlay(tiles: TileOverlay): Promise<any> {
return await CapacitorGoogleMaps.addTileOverlay({
id: this.id,
...tiles,
});
}

/**
* Adds a marker to the map
*
Expand Down
84 changes: 81 additions & 3 deletions google-maps/src/web.ts
Expand Up @@ -11,6 +11,7 @@ import {
import type { Marker } from './definitions';
import { MapType, LatLngBounds } from './definitions';
import type {
AddTileOverlayArgs,
AddMarkerArgs,
CameraArgs,
AddMarkersArgs,
Expand All @@ -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
Expand Down Expand Up @@ -132,7 +169,6 @@ export class CapacitorGoogleMapsWeb
});
const google = await loader.load();
this.gMapsRef = google.maps;
console.log('Loaded google maps API');
}
}

Expand Down Expand Up @@ -260,6 +296,50 @@ export class CapacitorGoogleMapsWeb
map.fitBounds(bounds, _args.padding);
}

async addTileOverlay(_args: AddTileOverlayArgs): Promise<any> {
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];
Expand Down Expand Up @@ -434,7 +514,6 @@ export class CapacitorGoogleMapsWeb
}

async create(_args: CreateMapArgs): Promise<void> {
console.log(`Create map: ${_args.id}`);
await this.importGoogleLib(_args.apiKey, _args.region, _args.language);

this.maps[_args.id] = {
Expand All @@ -449,7 +528,6 @@ export class CapacitorGoogleMapsWeb
}

async destroy(_args: DestroyMapArgs): Promise<void> {
console.log(`Destroy map: ${_args.id}`);
const mapItem = this.maps[_args.id];
mapItem.element.innerHTML = '';
mapItem.map.unbindAll();
Expand Down