diff --git a/android/build.gradle b/android/build.gradle index 7f92be6..84a86d2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -34,6 +34,10 @@ android { lintOptions { abortOnError false } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } repositories { diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CapacitorGoogleMaps.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CapacitorGoogleMaps.java index 5f63c2e..1bc609a 100644 --- a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CapacitorGoogleMaps.java +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CapacitorGoogleMaps.java @@ -585,4 +585,49 @@ public void run() { } }); } + + @PluginMethod() + public void addPolyline(final PluginCall call) { + final String mapId = call.getString("mapId"); + + getBridge().getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + CustomMapView customMapView = customMapViews.get(mapId); + + if (customMapView != null) { + CustomPolyline customPolyline = new CustomPolyline(); + customPolyline.updateFromJSObject(call.getData()); + + customMapView.addPolyline(customPolyline, (polyline) -> { + call.resolve(customPolyline.getResultForPolyline(polyline, mapId)); + }); + } else { + call.reject("map not found"); + } + } + }); + } + + @PluginMethod(returnType = PluginMethod.RETURN_NONE) + public void removePolyline(final PluginCall call) { + final String mapId = call.getString("mapId"); + + getBridge().getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + CustomMapView customMapView = customMapViews.get(mapId); + + if (customMapView != null) { + final String polylineId = call.getString("polylineId"); + + customMapView.removePolyline(polylineId); + + call.resolve(); + } else { + call.reject("map not found"); + } + } + }); + } } diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapView.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapView.java index f23b6a0..68faa9e 100644 --- a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapView.java +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapView.java @@ -26,6 +26,7 @@ import com.google.android.libraries.maps.model.Marker; import com.google.android.libraries.maps.model.PointOfInterest; import com.google.android.libraries.maps.model.Polygon; +import com.google.android.libraries.maps.model.Polyline; import java.util.HashMap; import java.util.Map; @@ -56,6 +57,7 @@ public class CustomMapView private HashMap markers = new HashMap<>(); private HashMap polygons = new HashMap<>(); + private HashMap polylines = new HashMap<>(); String savedCallbackIdForCreate; @@ -495,6 +497,29 @@ public void removePolygon(String polygonId) { } } + public void addPolyline(CustomPolyline customPolyline, @Nullable Consumer consumer) { + customPolyline.addToMap( + googleMap, + (polyline) -> { + polylines.put(customPolyline.polylineId, polyline); + + if (consumer != null) { + consumer.accept(polyline); + } + } + ); + } + + public void removePolyline(String polylineId) { + Polyline polyline = polylines.get(polylineId); + + if (polyline != null) { + polyline.remove(); + polylines.remove(polylineId); + } + } + + private JSObject getResultForMap() { if (this.mapView != null && this.googleMap != null) { diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomPolyline.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomPolyline.java new file mode 100644 index 0000000..ba0425a --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomPolyline.java @@ -0,0 +1,172 @@ +package com.hemangkumar.capacitorgooglemaps; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Consumer; + +import com.getcapacitor.JSArray; +import com.getcapacitor.JSObject; +import com.getcapacitor.util.WebColor; +import com.google.android.libraries.maps.GoogleMap; +import com.google.android.libraries.maps.model.LatLng; +import com.google.android.libraries.maps.model.Polyline; +import com.google.android.libraries.maps.model.PolylineOptions; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +public class CustomPolyline { + // generate id for the just added polyline, + // put this polyline into a hashmap with the corresponding id, + // so we can retrieve the polyline by id later on + public final String polylineId = UUID.randomUUID().toString(); + + private final PolylineOptions polylineOptions = new PolylineOptions(); + protected JSObject tag = new JSObject(); + + public void updateFromJSObject(JSObject polyline) { + this.setPath(polyline.optJSONArray("path")); + + final JSObject preferences = JSObjectDefaults.getJSObjectSafe(polyline, "preferences", new JSObject()); + + this.setBasicFields(preferences); + this.setMetadata(JSObjectDefaults.getJSObjectSafe(preferences, "metadata", new JSObject())); + } + + public void addToMap(GoogleMap googleMap, @Nullable Consumer consumer) { + final Polyline polyline = googleMap.addPolyline(polylineOptions); + polyline.setTag(tag); + + if (consumer != null) { + consumer.accept(polyline); + } + } + + private void setPath(@Nullable JSONArray path) { + if (path != null) { + List latLngList = getLatLngList(path); + this.polylineOptions.addAll(latLngList); + } + } + + private static List getLatLngList(@NonNull JSONArray latLngArray) { + List latLngList = new ArrayList<>(); + + for (int n = 0; n < latLngArray.length(); n++) { + JSONObject latLngObject = latLngArray.optJSONObject(n); + if (latLngObject != null) { + LatLng latLng = getLatLng(latLngObject); + latLngList.add(latLng); + } + } + + return latLngList; + } + + private static LatLng getLatLng(@NonNull JSONObject latLngObject) { + double latitude = latLngObject.optDouble("latitude", 0d); + double longitude = latLngObject.optDouble("longitude", 0d); + return new LatLng(latitude, longitude); + } + + private void setBasicFields(@NonNull JSObject preferences) { + final float width = (float) preferences.optDouble("width", 10); + final int color = WebColor.parseColor(preferences.optString("color", "#000000")); + final float zIndex = (float) preferences.optDouble("zIndex", 0); + final boolean isVisible = preferences.optBoolean("isVisible", true); + final boolean isGeodesic = preferences.optBoolean("isGeodesic", false); + final boolean isClickable = preferences.optBoolean("isClickable", false); + + polylineOptions.color(color); + polylineOptions.width(width); + polylineOptions.zIndex(zIndex); + polylineOptions.visible(isVisible); + polylineOptions.geodesic(isGeodesic); + polylineOptions.clickable(isClickable); + } + + private void setMetadata(@NonNull JSObject jsObject) { + JSObject tag = new JSObject(); + tag.put("id", this.polylineId); + tag.put("metadata", jsObject); + this.tag = tag; + } + + public JSObject getResultForPolyline(Polyline polyline, String mapId) { + JSObject tag = null; + + try { + tag = (JSObject) polyline.getTag(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + tag = tag != null ? tag : new JSObject(); + } + + // initialize JSObjects to return + JSObject result = new JSObject(); + JSObject polylineResult = new JSObject(); + JSObject preferencesResult = new JSObject(); + + result.put("polyline", polylineResult); + polylineResult.put("preferences", preferencesResult); + + // get map id + polylineResult.put("mapId", mapId); + + // get id + String polylineId = tag.optString("polylineId", polyline.getId()); + polylineResult.put("polylineId", polylineId); + + // get path values + JSArray path = latLngsToJSArray(polyline.getPoints()); + polylineResult.put("path", path); + + // get preferences + preferencesResult.put("width", polyline.getWidth()); + preferencesResult.put("color", colorToString(polyline.getColor())); + preferencesResult.put("zIndex", polyline.getZIndex()); + preferencesResult.put("isVisible", polyline.isVisible()); + preferencesResult.put("isGeodesic", polyline.isGeodesic()); + preferencesResult.put("isClickable", polyline.isClickable()); + + JSObject metadata = JSObjectDefaults.getJSObjectSafe(tag, "metadata", new JSObject()); + preferencesResult.put("metadata", metadata); + + return result; + } + + private static JSArray latLngsToJSArray(Collection positions) { + JSArray jsPositions = new JSArray(); + for (LatLng pos : positions) { + JSObject jsPos = latLngToJSObject(pos); + jsPositions.put(jsPos); + } + return jsPositions; + } + + private static JSObject latLngToJSObject(LatLng latLng) { + JSObject jsPos = new JSObject(); + jsPos.put("latitude", latLng.latitude); + jsPos.put("longitude", latLng.longitude); + return jsPos; + } + + private static String colorToString(int color) { + int r = ((color >> 16) & 0xff); + int g = ((color >> 8) & 0xff); + int b = ((color) & 0xff); + int a = ((color >> 24) & 0xff); + if (a != 255) { + return String.format("#%02X%02X%02X%02X", a, r, g, b); + } else { + return String.format("#%02X%02X%02X", r, g, b); + } + } + +} diff --git a/ios/Plugin/CustomPolyline.swift b/ios/Plugin/CustomPolyline.swift new file mode 100644 index 0000000..47f515b --- /dev/null +++ b/ios/Plugin/CustomPolyline.swift @@ -0,0 +1,83 @@ +import Foundation +import GoogleMaps +import Capacitor + +class CustomPolyline : GMSPolyline { + var id: String! = NSUUID().uuidString.lowercased(); + + public func updateFromJSObject(_ polylineData: JSObject) { + let pathArray = polylineData["path"] as? [JSObject] ?? [JSObject]() + let path = CustomPolyline.pathFromJson(pathArray) + self.path = path + + let preferences = polylineData["preferences"] as? JSObject ?? JSObject() + + self.strokeWidth = preferences["width"] as? Double ?? 10.0 + + if let color = preferences["color"] as? String { + self.strokeColor = UIColor.capacitor.color(fromHex: color) ?? UIColor.black + } + + self.title = preferences["title"] as? String ?? "" + self.zIndex = Int32.init(preferences["zIndex"] as? Int ?? 1) + self.geodesic = preferences["isGeodesic"] as? Bool ?? false + self.isTappable = preferences["isClickable"] as? Bool ?? false + + let metadata: JSObject = preferences["metadata"] as? JSObject ?? JSObject() + self.userData = [ + "polylineId": self.id!, + "metadata": metadata + ] as? JSObject ?? JSObject() + } + + public static func getResultForPolyline(_ polyline: GMSPolyline, mapId: String) -> PluginCallResultData { + let tag: JSObject = polyline.userData as! JSObject + + return [ + "polyline": [ + "mapId": mapId, + "polylineId": tag["polylineId"] ?? "", + "path": CustomPolyline.jsonFromPath(polyline.path), + "preferences": [ + "title": polyline.title ?? "", + "width": polyline.strokeWidth, + "color": polyline.strokeColor ?? "", + "zIndex": polyline.zIndex, + "isGeodesic": polyline.geodesic, + "isClickable": polyline.isTappable, + "metadata": tag["metadata"] ?? JSObject() + ] + ] + ]; + } + + + private static func jsonFromPath(_ path: GMSPath?) -> [JSObject] { + guard let path = path else { + return [JSObject]() + } + let size = path.count() + var result: [JSObject] = [] + for i in stride(from: 0, to: size, by: 1) { + let coord = path.coordinate(at: i) + result.append(CustomPolyline.jsonFromCoord(coord)) + } + return result + } + + private static func jsonFromCoord(_ coord: CLLocationCoordinate2D) -> JSObject { + return ["latitude" : coord.latitude, "longitude": coord.longitude] + } + + private static func pathFromJson(_ latLngArray: [JSObject]) -> GMSPath { + let path = GMSMutablePath() + latLngArray.forEach { point in + if let lat = point["latitude"] as? Double, let long = point["longitude"] as? Double { + let coord = CLLocationCoordinate2D(latitude: lat, longitude: long) + path.add(coord) + } + } + + return path as GMSPath + } +} diff --git a/ios/Plugin/Plugin.m b/ios/Plugin/Plugin.m index 617794f..77861f2 100644 --- a/ios/Plugin/Plugin.m +++ b/ios/Plugin/Plugin.m @@ -15,6 +15,8 @@ CAP_PLUGIN_METHOD(removeMarker, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(addPolygon, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(removePolygon, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(addPolyline, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(removePolyline, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(didTapInfoWindow, CAPPluginReturnCallback); CAP_PLUGIN_METHOD(didCloseInfoWindow, CAPPluginReturnCallback); CAP_PLUGIN_METHOD(didTapMap, CAPPluginReturnCallback); diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 1304830..3206318 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -11,6 +11,8 @@ public class CapacitorGoogleMaps: CustomMapViewEvents { var customPolygons = [String: CustomPolygon](); + var customPolylines = [String: CustomPolyline](); + var customWebView: CustomWKWebView? @objc func initialize(_ call: CAPPluginCall) { @@ -330,6 +332,43 @@ public class CapacitorGoogleMaps: CustomMapViewEvents { } } + @objc func addPolyline(_ call: CAPPluginCall) { + let mapId: String = call.getString("mapId", ""); + + DispatchQueue.main.async { + guard let customMapView = self.customWebView?.customMapViews[mapId] else { + call.reject("map not found") + return + } + + if let path = call.getArray("path")?.capacitor.replacingNullValues() as? [JSObject?] { + let preferences = call.getObject("preferences", JSObject()) + + self.addPolyline([ + "path": path, + "preferences": preferences + ], customMapView: customMapView) { polyline in + call.resolve(CustomPolyline.getResultForPolyline(polyline, mapId: mapId)) + } + } + } + } + + @objc func removePolyline(_ call: CAPPluginCall) { + let polylineId: String = call.getString("polylineId", ""); + + DispatchQueue.main.async { + if let customPolyline = self.customPolylines[polylineId] { + customPolyline.map = nil; + customPolyline.layer.removeFromSuperlayer() + self.customPolylines[polylineId] = nil; + call.resolve(); + } else { + call.reject("polyline not found"); + } + } + } + @objc func didTapInfoWindow(_ call: CAPPluginCall) { setCallbackIdForEvent(call: call, eventName: CustomMapView.EVENT_DID_TAP_INFO_WINDOW); } @@ -465,6 +504,21 @@ private extension CapacitorGoogleMaps { completion(polygon) } } + + func addPolyline(_ polylineData: JSObject, customMapView: CustomMapView, completion: @escaping VoidReturnClosure) { + DispatchQueue.main.async { + let polyline = CustomPolyline() + + polyline.updateFromJSObject(polylineData) + + polyline.map = customMapView.GMapView + + self.customPolylines[polyline.id] = polyline + + completion(polyline) + } + } + diff --git a/src/definitions.ts b/src/definitions.ts index 86d9605..5de2249 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -19,6 +19,9 @@ import { AddPolygonOptions, AddPolygonResult, RemovePolygonOptions, + AddPolylineOptions, + AddPolylineResult, + RemovePolylineOptions, // events DidTapInfoWindowCallback, DidCloseInfoWindowCallback, @@ -78,6 +81,10 @@ export interface CapacitorGoogleMapsPlugin { removePolygon(options: RemovePolygonOptions): Promise; + addPolyline(options: AddPolylineOptions): Promise; + + removePolyline(options: RemovePolylineOptions): Promise; + didTapInfoWindow( options: DefaultEventOptions, callback: DidTapInfoWindowCallback diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 2f1146f..bd72064 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -15,6 +15,8 @@ export { export { RemoveMarkerOptions } from "./methods/RemoveMarker"; export { AddPolygonOptions, AddPolygonResult } from "./methods/AddPolygon"; export { RemovePolygonOptions } from "./methods/RemovePolygon"; +export { AddPolylineOptions, AddPolylineResult } from "./methods/AddPolyline"; +export { RemovePolylineOptions } from "./methods/RemovePolyline"; // events export * from "./events/DidTapInfoWindow"; @@ -47,5 +49,7 @@ export { MapPreferences } from "./models/GoogleMap/Preferences"; export { PointOfInterest } from "./models/GoogleMap/PointOfInterest"; export { Polygon } from "./models/GoogleMap/Polygon/Polygon"; export { PolygonPreferences } from "./models/GoogleMap/Polygon/PolygonPreferences"; +export { Polyline } from "./models/GoogleMap/Polyline/Polyline"; +export { PolylinePreferences } from "./models/GoogleMap/Polyline/PolylinePreferences"; export { BoundingRect } from "./models/BoundingRect"; export { LatLng } from "./models/LatLng"; diff --git a/src/interfaces/methods/AddPolyline.ts b/src/interfaces/methods/AddPolyline.ts new file mode 100644 index 0000000..1111a2a --- /dev/null +++ b/src/interfaces/methods/AddPolyline.ts @@ -0,0 +1,27 @@ +import { Polyline, PolylinePreferences, LatLng } from "../../definitions"; + +export interface AddPolylineOptions { + /** + * GUID representing the map this polyline is a part of + * + * @since 2.0.0 + */ + mapId: string; + /** + * Line segments are drawn between consecutive points in the shorter of the two directions + * + * @since 2.0.0 + */ + path: LatLng[]; + /** + * @since 2.0.0 + */ + preferences?: PolylinePreferences; +} + +export interface AddPolylineResult { + /** + * @since 2.0.0 + */ + polyline: Polyline; +} diff --git a/src/interfaces/methods/RemovePolyline.ts b/src/interfaces/methods/RemovePolyline.ts new file mode 100644 index 0000000..96f46d0 --- /dev/null +++ b/src/interfaces/methods/RemovePolyline.ts @@ -0,0 +1,11 @@ +export interface RemovePolylineOptions { + /** + * @since 2.0.0 + */ + mapId: string; + /** + * @since 2.0.0 + */ + polylineId: string; + } + \ No newline at end of file diff --git a/src/interfaces/models/GoogleMap/Polyline/Polyline.ts b/src/interfaces/models/GoogleMap/Polyline/Polyline.ts new file mode 100644 index 0000000..b147a56 --- /dev/null +++ b/src/interfaces/models/GoogleMap/Polyline/Polyline.ts @@ -0,0 +1,26 @@ +import { LatLng, PolylinePreferences } from "../../../../definitions"; + +export interface Polyline { + /** + * GUID representing the map this polyline is a part of + * + * @since 2.0.0 + */ + mapId: string; + /** + * GUID representing the unique id of this polyline + * + * @since 2.0.0 + */ + polylineId: string; + /** + * Line segments are drawn between consecutive points in the shorter of the two directions. + * + * @since 2.0.0 + */ + path: LatLng[]; + /** + * @since 2.0.0 + */ + preferences?: PolylinePreferences; +} diff --git a/src/interfaces/models/GoogleMap/Polyline/PolylinePreferences.ts b/src/interfaces/models/GoogleMap/Polyline/PolylinePreferences.ts new file mode 100644 index 0000000..bc9ed6d --- /dev/null +++ b/src/interfaces/models/GoogleMap/Polyline/PolylinePreferences.ts @@ -0,0 +1,66 @@ +export interface PolylinePreferences { + + /** + * Line segment width in screen pixels. + * The width is constant and independent of the camera's zoom level. + * + * @default 10 + * @since 2.0.0 + */ + width?: number; + + /** + * Line segment color in HEX format (with transparency). + * + * @default #000000 (black) + * @since 2.0.0 + */ + color?: string; + + /** + * The z-index specifies the stack order of this polyline, relative to other polylines on the map. + * A polyline with a higher z-index is drawn on top of those with lower indices. + * Markers are always drawn above tile layers and other non-marker overlays (ground overlays, + * polylines, polylines, and other shapes) regardless of the z-index of the other overlays. + * Markers are effectively considered to be in a separate z-index group compared to other overlays. + */ + zIndex?: number; + + /** + * Sets the visibility of this polyline. + * When not visible, a polyline is not drawn, but it keeps all its other properties. + * + * @default true + * @since 2.0.0 + */ + isVisible?: boolean; + + /** + * If `true`, then each segment is drawn as a geodesic. + * If `false`, each segment is drawn as a straight line on the Mercator projection. + * + * @default false + * @since 2.0.0 + */ + isGeodesic?: boolean; + + /** + * If you want to handle events fired when the user clicks the polyline, set this property to `true`. + * You can change this value at any time. + * + * @default false + * @since 2.0.0 + */ + isClickable?: boolean; + + /** + * You can use this property to associate an arbitrary object with this overlay. + * The Google Maps SDK neither reads nor writes this property. + * Note that metadata should not hold any strong references to any Maps objects, + * otherwise a retain cycle may be created (preventing objects from being released). + * + * @default {} + * @since 2.0.0 + */ + metadata?: { [key: string]: any }; +} diff --git a/src/web.ts b/src/web.ts index 517e4f6..f96e24e 100644 --- a/src/web.ts +++ b/src/web.ts @@ -20,6 +20,9 @@ import { AddPolygonOptions, AddPolygonResult, RemovePolygonOptions, + AddPolylineOptions, + AddPolylineResult, + RemovePolylineOptions, DidTapInfoWindowCallback, DidCloseInfoWindowCallback, DidTapMapCallback, @@ -93,6 +96,14 @@ export class CapacitorGoogleMapsWeb throw this.unimplemented("Not implemented on web."); } + async addPolyline(_options: AddPolylineOptions): Promise { + throw this.unimplemented("Not implemented on web."); + } + + async removePolyline(_options: RemovePolylineOptions): Promise { + throw this.unimplemented("Not implemented on web."); + } + async didTapInfoWindow( _options: DefaultEventOptions, _callback: DidTapInfoWindowCallback