diff --git a/google-maps/README.md b/google-maps/README.md index 0d858c28b..2e429257f 100644 --- a/google-maps/README.md +++ b/google-maps/README.md @@ -296,6 +296,9 @@ export default MyMap; * [`removeCircles(...)`](#removecircles) * [`addPolylines(...)`](#addpolylines) * [`removePolylines(...)`](#removepolylines) +* [`addFeatures(...)`](#addfeatures) +* [`getFeatureBounds(...)`](#getfeaturebounds) +* [`removeFeature(...)`](#removefeature) * [`destroy()`](#destroy) * [`setCamera(...)`](#setcamera) * [`getMapType()`](#getmaptype) @@ -527,6 +530,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 @@ -1016,6 +1065,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 @@ -1158,6 +1212,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 a22aa2208..7133ce166 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 @@ -324,6 +336,78 @@ 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() + if (id != null) { + featureIds.add(id) + featureLayers[id] = featureLayer + } + callback(Result.success(featureIds)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + } else { + callback(Result.failure(InvalidArgumentsError("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(InvalidArgumentsError("getFeatureBounds: not supported for this feature type"))) + } + } else { + callback(Result.failure(InvalidArgumentsError("Could not find feature for feature id $featureId"))) + } + } + } catch(e: Exception) { + callback(Result.failure(InvalidArgumentsError("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) + } else { + callback(InvalidArgumentsError("Could not find feature for feature id $featureId")) + } + } + } + private fun setClusterManagerRenderer(minClusterSize: Int?) { clusterManager?.renderer = CapacitorClusterManagerRenderer( delegate.bridge.context, @@ -941,6 +1025,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 000000000..579e3123e --- /dev/null +++ b/google-maps/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsFeatureLayer.kt @@ -0,0 +1,75 @@ +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? = null + var layer: Layer? = null + + init { + (feature as? GeoJsonFeature)?.let { + val properties: HashMap = hashMapOf() + for (propertyKey in feature.propertyKeys) { + properties[propertyKey] = feature.getProperty(propertyKey) + } + if (idPropertyName != null) { + id = feature.getProperty(idPropertyName) + } + val feature = + GeoJsonFeature( + feature.geometry, + id, + properties, + null + ) + this.layer = layer + + val featureLayer = (layer as GeoJsonLayer); + featureLayer.addFeature(feature) + + if (styles != null) { + 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 143eca887..779fc611b 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 @@ -630,6 +630,134 @@ class CapacitorGoogleMapsPlugin : Plugin(), OnMapsSdkInitializedCallback { } } + @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") ?: null + + val styles = call.getObject("styles") ?: null + + map.addFeatures( + type, + data, + idPropertyName, + styles + ) { result -> + try { + 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) + } + } + } 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 -> + try { + 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) + } + } + } 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) { + try { + call.resolve() + } catch (e: GoogleMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + } 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 4ae033d08..097331653 100644 --- a/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.m +++ b/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.m @@ -20,6 +20,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 59af66ba8..bd42c28d3 100644 --- a/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.swift +++ b/google-maps/ios/Plugin/CapacitorGoogleMapsPlugin.swift @@ -511,6 +511,98 @@ 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") + } + + let idPropertyName = call.getString("idPropertyName") + + let styles = call.getObject("styles") + + 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.5) + + 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 71e4a7de9..1cf9f4161 100644 --- a/google-maps/ios/Plugin/Map.swift +++ b/google-maps/ios/Plugin/Map.swift @@ -78,6 +78,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 identifier_name @@ -403,6 +404,87 @@ 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 { + var featureId: String? = nil + if (idPropertyName != nil) { + featureId = propertes[idPropertyName!] as? String + + if (featureId != nil) { + 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) + + if (featureId != nil) { + featureIds.append(featureId!) + } + + if (featureId != nil && styles != nil) { + 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() + + if (featureId != nil) { + 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 @@ -732,3 +814,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 6fa91254a..4af8d015d 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 76555ada0..b6f8c3eb5 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; @@ -183,6 +204,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 aaf949478..1c369f1aa 100644 --- a/google-maps/src/index.ts +++ b/google-maps/src/index.ts @@ -7,6 +7,8 @@ import { Circle, Polyline, StyleSpan, + FeatureType, + FeatureStyles, } from './definitions'; import { GoogleMap } from './map'; @@ -19,6 +21,8 @@ export { Circle, Polyline, StyleSpan, + FeatureType, + FeatureStyles }; declare global { diff --git a/google-maps/src/map.ts b/google-maps/src/map.ts index 67b6324e6..5cbb552b3 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'; @@ -48,6 +50,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; /** @@ -475,6 +485,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 5b82addec..53507bf7b 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 { AddMarkerArgs, CameraArgs, @@ -33,6 +33,9 @@ import type { RemoveCirclesArgs, AddPolylinesArgs, RemovePolylinesArgs, + AddFeatureArgs, + GetFeatureBoundsArgs, + RemoveFeatureArgs, } from './implementation'; export class CapacitorGoogleMapsWeb @@ -399,6 +402,88 @@ 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, + args.idPropertyName + ? { + idPropertyName: args.idPropertyName, + } + : null, + ) + .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()); + } + } + + if (args.styles) { + 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 }> { + if (!args.featureId) { + throw new Error(`Feature id not set.`); + } + + const map = this.maps[args.id]; + const feature = map.map.data.getFeatureById(args.featureId); + + if (!feature) { + throw new Error(`Feature '${args.featureId}' could not be found.`); + } + + const bounds = new google.maps.LatLngBounds(); + + feature?.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 { + if (!args.featureId) { + throw new Error(`Feature id not set.`); + } + + const map = this.maps[args.id]; + + const feature = map.map.data.getFeatureById(args.featureId); + if (!feature) { + throw new Error(`Feature '${args.featureId}' could not be found.`); + } + + map.map.data.remove(feature); + } + async enableClustering(_args: EnableClusteringArgs): Promise { const markers: google.maps.Marker[] = [];