From adb72af4055c8f8daef0fc888b7aa384717ecce4 Mon Sep 17 00:00:00 2001 From: "jgessinger@aeins.de" Date: Wed, 17 May 2023 16:31:55 +0200 Subject: [PATCH] feat(google-maps): Implemented map feature functions (i.e. GeoJSON support) --- google-maps/README.md | 62 +++++++++ .../plugins/googlemaps/CapacitorGoogleMap.kt | 130 +++++++++++++++++- .../CapacitorGoogleMapsFeatureLayer.kt | 68 +++++++++ .../googlemaps/CapacitorGoogleMapsPlugin.kt | 114 +++++++++++++++ .../ios/Plugin/CapacitorGoogleMapsPlugin.m | 3 + .../Plugin/CapacitorGoogleMapsPlugin.swift | 96 +++++++++++++ google-maps/ios/Plugin/Map.swift | 105 ++++++++++++++ google-maps/src/definitions.ts | 28 ++++ google-maps/src/implementation.ts | 26 ++++ google-maps/src/index.ts | 16 ++- google-maps/src/map.ts | 43 ++++++ google-maps/src/web.ts | 69 +++++++++- 12 files changed, 756 insertions(+), 4 deletions(-) create mode 100644 google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsFeatureLayer.kt diff --git a/google-maps/README.md b/google-maps/README.md index 70e56e7e7f..4e99231373 100644 --- a/google-maps/README.md +++ b/google-maps/README.md @@ -288,6 +288,9 @@ export default MyMap; * [`removeCircles(...)`](#removecircles) * [`addPolylines(...)`](#addpolylines) * [`removePolylines(...)`](#removepolylines) +* [`addFeatures(...)`](#addfeatures) +* [`getFeatureBounds(...)`](#getfeaturebounds) +* [`removeFeature(...)`](#removefeature) * [`destroy()`](#destroy) * [`setCamera(...)`](#setcamera) * [`getMapType()`](#getmaptype) @@ -500,6 +503,52 @@ removePolylines(ids: string[]) => Promise -------------------- +### addFeatures(...) + +```typescript +addFeatures(type: FeatureType, data: any, idPropertyName: string, styles: FeatureStyles) => Promise +``` + +| Param | Type | +| -------------------- | ------------------------------------------------------- | +| **`type`** | FeatureType | +| **`data`** | any | +| **`idPropertyName`** | string | +| **`styles`** | FeatureStyles | + +**Returns:** Promise<string[]> + +-------------------- + + +### getFeatureBounds(...) + +```typescript +getFeatureBounds(featureId: string) => Promise +``` + +| Param | Type | +| --------------- | ------------------- | +| **`featureId`** | string | + +**Returns:** Promise<LatLngBounds> + +-------------------- + + +### removeFeature(...) + +```typescript +removeFeature(featureId: string) => Promise +``` + +| Param | Type | +| --------------- | ------------------- | +| **`featureId`** | string | + +-------------------- + + ### destroy() ```typescript @@ -970,6 +1019,11 @@ Describes the style for some region of a polyline. | **`segments`** | number | The length of this span in number of segments. | +#### FeatureStyles + +Feature styles, identified by the feature id + + #### CameraConfig Configuration properties for a Google Map Camera @@ -1112,6 +1166,14 @@ but the current specification only allows X, Y, and (optionally) Z to be defined ### Enums +#### FeatureType + +| Members | Value | Description | +| ------------- | ---------------------- | ----------- | +| **`Default`** | 'Default' | Default | +| **`GeoJSON`** | 'GeoJSON' | GeoJSON | + + #### MapType | Members | Value | Description | 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 80fa64a96f..4a8a41ddae 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 @@ -16,8 +16,19 @@ import com.google.android.gms.maps.GoogleMap.* import com.google.android.gms.maps.model.* import com.google.maps.android.clustering.Cluster import com.google.maps.android.clustering.ClusterManager +import com.google.maps.android.data.Feature +import com.google.maps.android.data.geojson.GeoJsonFeature +import com.google.maps.android.data.geojson.GeoJsonGeometryCollection +import com.google.maps.android.data.geojson.GeoJsonLayer +import com.google.maps.android.data.geojson.GeoJsonLineString +import com.google.maps.android.data.geojson.GeoJsonMultiLineString +import com.google.maps.android.data.geojson.GeoJsonMultiPoint +import com.google.maps.android.data.geojson.GeoJsonMultiPolygon +import com.google.maps.android.data.geojson.GeoJsonPoint +import com.google.maps.android.data.geojson.GeoJsonPolygon import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel +import org.json.JSONObject import java.io.InputStream import java.net.URL @@ -45,7 +56,8 @@ class CapacitorGoogleMap( private val markers = HashMap() private val polygons = HashMap() private val circles = HashMap() - private val polylines = HashMap() + private val polylines = HashMap() + private val featureLayers = HashMap() private val markerIcons = HashMap() private var clusterManager: ClusterManager? = null @@ -317,6 +329,75 @@ class CapacitorGoogleMap( } } + fun addFeatures(type: String, data: JSONObject, idPropertyName: String, styles: JSONObject, callback: (ids: Result>) -> Unit) { + try { + googleMap ?: throw GoogleMapNotAvailable() + val featureIds: MutableList = mutableListOf() + + CoroutineScope(Dispatchers.Main).launch { + if (type == "GeoJSON") { + val tempLayer = GeoJsonLayer(null, data) + tempLayer.features.forEach { + try { + val layer = GeoJsonLayer(googleMap, JSONObject()) + val featureLayer = CapacitorGoogleMapsFeatureLayer(layer, it, idPropertyName, styles) + layer.addLayerToMap() + featureIds.add(featureLayer.id) + featureLayers[featureLayer.id] = featureLayer + callback(Result.success(featureIds)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + } else { + callback(Result.failure(Error("addFeatures: not supported for this feature type"))) + } + } + } catch (e: GoogleMapsError) { + callback(Result.failure(e)) + } + } + + fun getFeatureBounds(featureId: String, callback: (bounds: Result) -> Unit) { + try { + CoroutineScope(Dispatchers.Main).launch { + val featurelayer = featureLayers[featureId] + var feature: Feature? = null; + featurelayer?.layer?.features?.forEach lit@ { + if (it.id == featurelayer.id) { + feature = it + return@lit + } + } + if (feature != null) { + try { + (feature as GeoJsonFeature).let { + callback(Result.success(it.boundingBoxFromGeometry())) + } + } catch (e: Exception) { + callback(Result.failure(Error("getFeatureBounds: not supported for this feature type"))) + } + } else { + callback(Result.failure(Error("Could not find feature for feature id $featureId"))) + } + } + } catch(e: Exception) { + callback(Result.failure(Error("Could not find feature layer"))) + } + } + + fun removeFeature(featureId: String, callback: (error: GoogleMapsError?) -> Unit) { + CoroutineScope(Dispatchers.Main).launch { + val featurelayer = featureLayers[featureId] + if (featurelayer != null) { + featurelayer.layer?.removeLayerFromMap() + featureLayers.remove(featureId) + } + + callback(null) + } + } + private fun setClusterManagerRenderer(minClusterSize: Int?) { clusterManager?.renderer = CapacitorClusterManagerRenderer( delegate.bridge.context, @@ -925,6 +1006,53 @@ class CapacitorGoogleMap( return data } + + private fun GeoJsonFeature.boundingBoxFromGeometry(): LatLngBounds? { + val coordinates: MutableList = ArrayList() + + if (this.hasGeometry()) { + when (geometry.geometryType) { + "Point" -> coordinates.add((geometry as GeoJsonPoint).coordinates) + "MultiPoint" -> { + val points = (geometry as GeoJsonMultiPoint).points + for (point in points) { + coordinates.add(point.coordinates) + } + } + + "LineString" -> coordinates.addAll((geometry as GeoJsonLineString).coordinates) + "MultiLineString" -> { + val lines = (geometry as GeoJsonMultiLineString).lineStrings + for (line in lines) { + coordinates.addAll(line.coordinates) + } + } + + "Polygon" -> { + val lists = (geometry as GeoJsonPolygon).coordinates + for (list in lists) { + coordinates.addAll(list) + } + } + + "MultiPolygon" -> { + val polygons = (geometry as GeoJsonMultiPolygon).polygons + for (polygon in polygons) { + for (list in polygon.coordinates) { + coordinates.addAll(list) + } + } + } + } + } + + val builder = LatLngBounds.builder() + for (latLng in coordinates) { + builder.include(latLng) + } + return builder.build() + } + override fun onMapClick(point: LatLng) { val data = JSObject() data.put("mapId", this@CapacitorGoogleMap.id) diff --git a/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsFeatureLayer.kt b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsFeatureLayer.kt new file mode 100644 index 0000000000..9497102ea4 --- /dev/null +++ b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsFeatureLayer.kt @@ -0,0 +1,68 @@ +package com.capacitorjs.plugins.googlemaps + +import android.graphics.Color +import com.google.maps.android.data.Feature +import com.google.maps.android.data.Layer +import com.google.maps.android.data.geojson.GeoJsonFeature +import com.google.maps.android.data.geojson.GeoJsonLayer +import org.json.JSONObject +import java.lang.Exception + +class CapacitorGoogleMapsFeatureLayer( + layer: Layer, + feature: Feature, + idPropertyName: String, + styles: JSONObject +) { + var id: String = "" + var layer: Layer? = null + + init { + (feature as? GeoJsonFeature)?.let { + val properties: HashMap = hashMapOf() + for (propertyKey in feature.propertyKeys) { + properties[propertyKey] = feature.getProperty(propertyKey) + } + val feature = + GeoJsonFeature( + feature.geometry, + feature.getProperty(idPropertyName), + properties, + null + ) + id = feature.id + this.layer = layer + + val featureLayer = (layer as GeoJsonLayer); + featureLayer.addFeature(feature) + + try { + featureLayer.defaultPolygonStyle.strokeColor = + processColor(styles.getStyle("strokeColor"), styles.getStyle("strokeOpacity")) + featureLayer.defaultPolygonStyle.strokeWidth = styles.getStyle("strokeWeight") + featureLayer.defaultPolygonStyle.fillColor = + processColor(styles.getStyle("fillColor"), styles.getStyle("fillOpacity")) + featureLayer.defaultPolygonStyle.isGeodesic = styles.getStyle("geodesic") + featureLayer.defaultLineStringStyle.color = + featureLayer.defaultPolygonStyle.strokeColor + featureLayer.defaultLineStringStyle.isGeodesic = + featureLayer.defaultPolygonStyle.isGeodesic + } catch (e: Exception) { + throw InvalidArgumentsError("Styles object contains invalid values") + } + } + } + + private fun processColor(hex: String, opacity: Double): Int { + val colorInt = Color.parseColor(hex) + + val alpha = (opacity * 255.0).toInt() + val red = Color.red(colorInt) + val green = Color.green(colorInt) + val blue = Color.blue(colorInt) + + return Color.argb(alpha, red, green, blue) + } + + private fun JSONObject.getStyle(key: String) = this.getJSONObject(id).get(key) as T +} 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 e4e908cf18..a107fc2bdd 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 @@ -589,6 +589,120 @@ class CapacitorGoogleMapsPlugin : Plugin() { } } + @PluginMethod + fun addFeatures(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val type = + call.getString("type") + ?: throw InvalidArgumentsError("feature type is missing") + + val data = + call.getObject("data") + ?: throw InvalidArgumentsError("feature data is missing") + + val idPropertyName = + call.getString("idPropertyName") + ?: throw InvalidArgumentsError("feature idPropertyName is missing") + + val styles = + call.getObject("styles") + ?: throw InvalidArgumentsError("feature styles missing") + + map.addFeatures( + type, + data, + idPropertyName, + styles + ) { result -> + val ids = result.getOrThrow() + + val jsonIDs = JSONArray() + ids.forEach { jsonIDs.put(it) } + + val res = JSObject() + res.put("ids", jsonIDs) + call.resolve(res) + } + } catch (e: GoogleMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun getFeatureBounds(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val featureId = + call.getString("featureId") + ?: throw InvalidArgumentsError("feature id is missing") + + map.getFeatureBounds(featureId) { result -> + val featureBounds = result.getOrThrow() + + val southwest = JSObject() + southwest.put("lat", featureBounds?.southwest?.latitude) + southwest.put("lng", featureBounds?.southwest?.longitude) + + val center = JSObject() + center.put("lat", featureBounds?.center?.latitude) + center.put("lng", featureBounds?.center?.longitude) + + val northeast = JSObject() + northeast.put("lat", featureBounds?.northeast?.latitude) + northeast.put("lng", featureBounds?.northeast?.longitude) + + val bounds = JSObject() + bounds.put("southwest", southwest) + bounds.put("center", center) + bounds.put("northeast", northeast) + + val res = JSObject() + res.put("bounds", bounds) + call.resolve(res) + } + } catch (e: GoogleMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun removeFeature(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val featureId = + call.getString("featureId") + ?: throw InvalidArgumentsError("feature id is missing") + + map.removeFeature(featureId) { + call.resolve() + } + } catch (e: GoogleMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + @PluginMethod fun setCamera(call: PluginCall) { try { diff --git a/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.m b/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.m index 9f659fa7f8..b5ee3c294a 100644 --- a/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.m +++ b/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.m @@ -18,6 +18,9 @@ CAP_PLUGIN_METHOD(enableClustering, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(disableClustering, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(destroy, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(addFeatures, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(getFeatureBounds, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(removeFeature, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setCamera, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getMapType, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setMapType, CAPPluginReturnPromise); diff --git a/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.swift b/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.swift index 6bcaf86ad2..9d70bf7efe 100644 --- a/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.swift +++ b/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.swift @@ -475,6 +475,102 @@ public class CapacitorGoogleMapsPlugin: CAPPlugin, GMSMapViewDelegate { } } + @objc func addFeatures(_ call: CAPPluginCall) { + do { + guard let id = call.getString("id") else { + throw GoogleMapErrors.invalidMapId + } + + guard let map = self.maps[id] else { + throw GoogleMapErrors.mapNotFound + } + + guard let type = call.getString("type") else { + throw GoogleMapErrors.invalidArguments("feature type is missing") + } + + guard let data = call.getObject("data") else { + throw GoogleMapErrors.invalidArguments("feature data is missing") + } + + guard let idPropertyName = call.getString("idPropertyName") else { + throw GoogleMapErrors.invalidArguments("feature idPropertyName is missing") + } + + guard let styles = call.getObject("styles") else { + throw GoogleMapErrors.invalidArguments("feature styles missing") + } + + let ids = try map.addFeatures(type: type, data: data, idPropertyName: idPropertyName, styles: styles) + + call.resolve(["ids": ids.map({ id in + return String(id) + })]) + } catch { + handleError(call, error: error) + } + } + + @objc func getFeatureBounds(_ call: CAPPluginCall) { + do { + guard let id = call.getString("id") else { + throw GoogleMapErrors.invalidMapId + } + + guard let map = self.maps[id] else { + throw GoogleMapErrors.mapNotFound + } + + guard let featureId = call.getString("featureId") else { + throw GoogleMapErrors.invalidArguments("feature id is missing") + } + + let bounds = try map.getFeatureBounds(featureId: featureId) + let center = GMSGeometryInterpolate(bounds.southWest, bounds.northEast, 0) + + call.resolve([ + "bounds": [ + "southwest": [ + "lat": bounds.southWest.latitude, + "lng": bounds.southWest.longitude + ], + "center": [ + "lat": center.latitude, + "lng": center.longitude + ], + "northeast": [ + "lat": bounds.northEast.latitude, + "lng": bounds.northEast.longitude + ] + ] + ]) + } catch { + handleError(call, error: error) + } + } + + @objc func removeFeature(_ call: CAPPluginCall) { + do { + guard let id = call.getString("id") else { + throw GoogleMapErrors.invalidMapId + } + + guard let map = self.maps[id] else { + throw GoogleMapErrors.mapNotFound + } + + guard let featureId = call.getString("featureId") else { + throw GoogleMapErrors.invalidArguments("feature id is missing") + } + + try map.removeFeature(featureId: featureId) + + call.resolve() + } catch { + handleError(call, error: error) + } + } + @objc func setCamera(_ call: CAPPluginCall) { do { guard let id = call.getString("id") else { diff --git a/google-maps/ios/Plugin/Map.swift b/google-maps/ios/Plugin/Map.swift index 75ff99b2dd..b58fbe3166 100644 --- a/google-maps/ios/Plugin/Map.swift +++ b/google-maps/ios/Plugin/Map.swift @@ -71,6 +71,7 @@ public class Map { var polygons = [Int: GMSPolygon]() var circles = [Int: GMSCircle]() var polylines = [Int: GMSPolyline]() + var features = [String: GMUGeometryRenderer]() var markerIcons = [String: UIImage]() // swiftlint:disable weak_delegate @@ -367,6 +368,74 @@ public class Map { } } + func addFeatures(type: String, data: JSObject, idPropertyName: String, styles: JSObject) throws -> [String] { + var featureIds: [String] = [] + + DispatchQueue.main.sync { + let jsonData : Data = try! JSONSerialization.data(withJSONObject: data, options: []) + let geoJSONParser = GMUGeoJSONParser(data: jsonData) + geoJSONParser.parse() + + for container in geoJSONParser.features { + if let tempFeature = container as? GMUFeature, let propertes = tempFeature.properties, let featureId = propertes[idPropertyName] as? String { + if let renderer = self.features[featureId] { + renderer.clear() + self.features.removeValue(forKey: featureId) + } + + let feature = GMUFeature(geometry: tempFeature.geometry, identifier: featureId, properties: tempFeature.properties, boundingBox: nil) + + featureIds.append(featureId) + + if let stylesData = (styles as [String: Any])[featureId] as? [String: Any] { + if let strokeColor = stylesData["strokeColor"] as? String, + let strokeOpacity = stylesData["strokeOpacity"] as? Double, + let stroke = UIColor.init(hex: strokeColor)?.withAlphaComponent(strokeOpacity), + let fillColor = stylesData["fillColor"] as? String, + let fillOpacity = stylesData["fillOpacity"] as? Double, + let fill = UIColor.init(hex: fillColor)?.withAlphaComponent(fillOpacity) + { + let style = GMUStyle(styleID: "styleId", stroke: stroke, fill: fill, width: stylesData["strokeWeight"] as? Double ?? 1, scale: 2, heading: 0, anchor: CGPoint(x: 0, y: 0), iconUrl: nil, title: nil, hasFill: true, hasStroke: true) + feature.style = style + } + } + + let renderer = GMUGeometryRenderer(map: self.mapViewController.GMapView, geometries: [feature]) + renderer.render() + + self.features[featureId] = renderer + } + } + } + + return featureIds + } + + func getFeatureBounds(featureId: String) throws -> GMSCoordinateBounds { + if let renderer = self.features[featureId] { + var bounds: GMSCoordinateBounds! + + DispatchQueue.main.sync { + bounds = renderer.getBounds() + } + + return bounds + } else { + throw GoogleMapErrors.unhandledError("feature not found") + } + } + + func removeFeature(featureId: String) throws { + if let renderer = self.features[featureId] { + DispatchQueue.main.async { + renderer.clear() + self.features.removeValue(forKey: featureId) + } + } else { + throw GoogleMapErrors.unhandledError("feature not found") + } + } + func setCamera(config: GoogleMapCameraConfig) throws { let currentCamera = self.mapViewController.GMapView.camera @@ -668,3 +737,39 @@ extension UIImage { return resizedImage } } + +extension GMUGeometryRenderer { + func getBounds() -> GMSCoordinateBounds { + var bounds = GMSCoordinateBounds.init() + + for overlay in self.mapOverlays() { + if let circle = overlay as? GMSCircle { + bounds = bounds.includingBounds(circle.getBounds()) + } + if let groundOverlay = overlay as? GMSGroundOverlay, let groundOverlayBounds = groundOverlay.bounds { + bounds = bounds.includingBounds(groundOverlayBounds) + } + if let marker = overlay as? GMSMarker { + bounds = bounds.includingCoordinate(marker.position) + } + if let polygon = overlay as? GMSPolygon, let polygonPath = polygon.path { + bounds = bounds.includingPath(polygonPath) + } + if let polyline = overlay as? GMSPolyline, let polylinePath = polyline.path { + bounds = bounds.includingPath(polylinePath) + } + } + + return bounds + } +} + +extension GMSCircle { + func getBounds() -> GMSCoordinateBounds { + var bounds = GMSCoordinateBounds.init( + coordinate: GMSGeometryOffset(self.position, self.radius * sqrt(2.0), 225), + coordinate: GMSGeometryOffset(self.position, self.radius * sqrt(2.0), 45) + ) + return bounds + } +} diff --git a/google-maps/src/definitions.ts b/google-maps/src/definitions.ts index 0c2c923280..7d3da68884 100644 --- a/google-maps/src/definitions.ts +++ b/google-maps/src/definitions.ts @@ -129,6 +129,34 @@ export interface Polyline extends google.maps.PolylineOptions { styleSpans?: StyleSpan[]; } +/** + * Feature types + */ +export enum FeatureType { + /** + * Default + */ + Default = 'Default', + /** + * GeoJSON + */ + GeoJSON = 'GeoJSON', +} + +/** + * Feature styles, identified by the feature id + */ +export interface FeatureStyles { + [key: string]: { + strokeColor: string; + strokeOpacity: number; + strokeWeight: number; + fillColor: string; + fillOpacity: number; + geodesic: boolean; + }; +} + /** * Describes the style for some region of a polyline. */ diff --git a/google-maps/src/implementation.ts b/google-maps/src/implementation.ts index a13a2fb4ae..4aca1dd78b 100644 --- a/google-maps/src/implementation.ts +++ b/google-maps/src/implementation.ts @@ -12,6 +12,8 @@ import type { Marker, Polygon, Polyline, + FeatureType, + FeatureStyles, } from './definitions'; /** @@ -92,6 +94,7 @@ export interface RemoveCirclesArgs { id: string; circleIds: string[]; } + export interface AddPolylinesArgs { id: string; polylines: Polyline[]; @@ -102,6 +105,24 @@ export interface RemovePolylinesArgs { polylineIds: string[]; } +export interface AddFeatureArgs { + id: string; + type: FeatureType; + data: any; + idPropertyName: string; + styles: FeatureStyles; +} + +export interface GetFeatureBoundsArgs { + id: string; + featureId: string; +} + +export interface RemoveFeatureArgs { + id: string; + featureId: string; +} + export interface CameraArgs { id: string; config: CameraConfig; @@ -175,6 +196,11 @@ export interface CapacitorGoogleMapsPlugin extends Plugin { removeCircles(args: RemoveCirclesArgs): Promise; addPolylines(args: AddPolylinesArgs): Promise<{ ids: string[] }>; removePolylines(args: RemovePolylinesArgs): Promise; + addFeatures(args: AddFeatureArgs): Promise<{ ids: string[] }>; + getFeatureBounds( + args: GetFeatureBoundsArgs, + ): Promise<{ bounds: LatLngBounds }>; + removeFeature(args: RemoveFeatureArgs): Promise; enableClustering(args: EnableClusteringArgs): Promise; disableClustering(args: { id: string }): Promise; destroy(args: DestroyMapArgs): Promise; diff --git a/google-maps/src/index.ts b/google-maps/src/index.ts index 6005f8a751..2c974ea8fd 100644 --- a/google-maps/src/index.ts +++ b/google-maps/src/index.ts @@ -6,10 +6,24 @@ import { Circle, Polyline, StyleSpan, + LatLngBounds, + FeatureType, + FeatureStyles, } from './definitions'; import { GoogleMap } from './map'; -export { GoogleMap, MapType, Marker, Polygon, Circle, Polyline, StyleSpan }; +export { + GoogleMap, + MapType, + Marker, + Polygon, + Circle, + Polyline, + StyleSpan, + LatLngBounds, + FeatureType, + FeatureStyles, +}; declare global { export namespace JSX { diff --git a/google-maps/src/map.ts b/google-maps/src/map.ts index 3e68c0fc39..ebfc0de26c 100644 --- a/google-maps/src/map.ts +++ b/google-maps/src/map.ts @@ -19,6 +19,8 @@ import type { CircleClickCallbackData, Polyline, PolylineCallbackData, + FeatureType, + FeatureStyles, } from './definitions'; import { LatLngBounds, MapType } from './definitions'; import type { CreateMapArgs } from './implementation'; @@ -46,6 +48,14 @@ export interface GoogleMapInterface { removeCircles(ids: string[]): Promise; addPolylines(polylines: Polyline[]): Promise; removePolylines(ids: string[]): Promise; + addFeatures( + type: FeatureType, + data: any, + idPropertyName: string, + styles: FeatureStyles, + ): Promise; + getFeatureBounds(featureId: string): Promise; + removeFeature(featureId: string): Promise; destroy(): Promise; setCamera(config: CameraConfig): Promise; /** @@ -363,6 +373,39 @@ export class GoogleMap { }); } + async addFeatures( + type: FeatureType, + data: any, + idPropertyName: string, + styles: FeatureStyles, + ): Promise { + const res = await CapacitorGoogleMaps.addFeatures({ + id: this.id, + type, + data, + idPropertyName, + styles, + }); + + return res.ids; + } + + async getFeatureBounds(id: string): Promise { + const res = await CapacitorGoogleMaps.getFeatureBounds({ + id: this.id, + featureId: id, + }); + + return new LatLngBounds(res.bounds); + } + + async removeFeature(id: string): Promise { + return CapacitorGoogleMaps.removeFeature({ + id: this.id, + featureId: id, + }); + } + /** * Destroy the current instance of the map */ diff --git a/google-maps/src/web.ts b/google-maps/src/web.ts index 862b040525..4e864b02c3 100644 --- a/google-maps/src/web.ts +++ b/google-maps/src/web.ts @@ -8,8 +8,8 @@ import { SuperClusterAlgorithm, } from '@googlemaps/markerclusterer'; -import type { Marker } from './definitions'; -import { MapType, LatLngBounds } from './definitions'; +import type { LatLngBoundsInterface, LatLng, Marker } from './definitions'; +import { FeatureType, MapType, LatLngBounds } from './definitions'; import type { AccElementsArgs, AddMarkerArgs, @@ -35,6 +35,9 @@ import type { RemoveCirclesArgs, AddPolylinesArgs, RemovePolylinesArgs, + AddFeatureArgs, + GetFeatureBoundsArgs, + RemoveFeatureArgs, } from './implementation'; export class CapacitorGoogleMapsWeb @@ -387,6 +390,68 @@ export class CapacitorGoogleMapsWeb } } + async addFeatures(args: AddFeatureArgs): Promise<{ ids: string[] }> { + const featureIds: string[] = []; + const map = this.maps[args.id]; + + if (args.type === FeatureType.GeoJSON) { + featureIds.push( + ...(map.map.data + .addGeoJson(args.data, { + idPropertyName: args.idPropertyName, + }) + .map(f => f.getId()) + .filter(f => f !== undefined) + .map(f => f?.toString()) as string[]), + ); + } else { + const featureId = map.map.data.add(args.data).getId(); + if (featureId) { + featureIds.push(featureId.toString()); + } + } + + map.map.data.setStyle(feature => { + const featureId = feature.getId(); + return featureId ? (args.styles[featureId] as any) : null; + }); + + return { + ids: featureIds, + }; + } + + async getFeatureBounds( + args: GetFeatureBoundsArgs, + ): Promise<{ bounds: LatLngBounds }> { + const map = this.maps[args.id]; + const bounds = new google.maps.LatLngBounds(); + + map.map.data + .getFeatureById(args.featureId) + ?.getGeometry() + ?.forEachLatLng(latLng => { + bounds.extend(latLng); + }); + + return { + bounds: new LatLngBounds({ + southwest: bounds.getSouthWest().toJSON() as LatLng, + center: bounds.getCenter().toJSON() as LatLng, + northeast: bounds.getNorthEast().toJSON() as LatLng, + } as LatLngBoundsInterface), + }; + } + + async removeFeature(args: RemoveFeatureArgs): Promise { + const map = this.maps[args.id]; + + const feature = map.map.data.getFeatureById(args.featureId); + if (feature) { + map.map.data.remove(feature); + } + } + async enableClustering(_args: EnableClusteringArgs): Promise { const markers: google.maps.Marker[] = [];