@capacitor/google-maps
Advanced tools
| // swiftlint:disable file_length | ||
| import Foundation | ||
| import Capacitor | ||
| import GoogleMaps | ||
| import GoogleMapsUtils | ||
| extension GMSMapViewType { | ||
| static func fromString(mapType: String) -> GMSMapViewType { | ||
| switch mapType { | ||
| case "Normal": | ||
| return .normal | ||
| case "Hybrid": | ||
| return .hybrid | ||
| case "Satellite": | ||
| return .satellite | ||
| case "Terrain": | ||
| return .terrain | ||
| case "None": | ||
| return .none | ||
| default: | ||
| print("CapacitorGoogleMaps Warning: unknown mapView type '\(mapType)'. Defaulting to normal.") | ||
| return .normal | ||
| } | ||
| } | ||
| static func toString(mapType: GMSMapViewType) -> String { | ||
| switch mapType { | ||
| case .normal: | ||
| return "Normal" | ||
| case .hybrid: | ||
| return "Hybrid" | ||
| case .satellite: | ||
| return "Satellite" | ||
| case .terrain: | ||
| return "Terrain" | ||
| case .none: | ||
| return "None" | ||
| default: | ||
| return "Normal" | ||
| } | ||
| } | ||
| } | ||
| extension CGRect { | ||
| static func fromJSObject(_ jsObject: JSObject) throws -> CGRect { | ||
| guard let width = jsObject["width"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("bounds object is missing the required 'width' property") | ||
| } | ||
| guard let height = jsObject["height"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("bounds object is missing the required 'height' property") | ||
| } | ||
| guard let x = jsObject["x"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("bounds object is missing the required 'x' property") | ||
| } | ||
| guard let y = jsObject["y"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("bounds object is missing the required 'y' property") | ||
| } | ||
| return CGRect(x: x, y: y, width: width, height: height) | ||
| } | ||
| } | ||
| // swiftlint:disable type_body_length | ||
| @objc(CapacitorGoogleMapsPlugin) | ||
| public class CapacitorGoogleMapsPlugin: CAPPlugin, GMSMapViewDelegate, CAPBridgedPlugin { | ||
| public let identifier = "CapacitorGoogleMapsPlugin" | ||
| public let jsName = "CapacitorGoogleMaps" | ||
| public let pluginMethods: [CAPPluginMethod] = [ | ||
| CAPPluginMethod(name: "create", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "enableTouch", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "disableTouch", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "addTileOverlay", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "removeTileOverlay", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "addMarker", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "addMarkers", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "addPolygons", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "addPolylines", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "addCircles", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "removeMarker", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "removeMarkers", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "removeCircles", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "removePolygons", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "removePolylines", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "enableClustering", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "disableClustering", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "destroy", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "setCamera", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "getMapType", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "setMapType", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "enableIndoorMaps", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "enableTrafficLayer", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "enableAccessibilityElements", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "enableCurrentLocation", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "setPadding", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "onScroll", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "onResize", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "onDisplay", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "getMapBounds", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "fitBounds", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "mapBoundsContains", returnType: CAPPluginReturnPromise), | ||
| CAPPluginMethod(name: "mapBoundsExtend", returnType: CAPPluginReturnPromise) | ||
| ] | ||
| private var maps = [String: Map]() | ||
| private var isInitialized = false | ||
| private var locationManager = CLLocationManager() | ||
| func checkLocationPermission() -> String { | ||
| let locationState: String | ||
| switch self.locationManager.authorizationStatus { | ||
| case .notDetermined: | ||
| locationState = "prompt" | ||
| case .restricted, .denied: | ||
| locationState = "denied" | ||
| case .authorizedAlways, .authorizedWhenInUse: | ||
| locationState = "granted" | ||
| @unknown default: | ||
| locationState = "prompt" | ||
| } | ||
| return locationState | ||
| } | ||
| @objc func create(_ call: CAPPluginCall) { | ||
| do { | ||
| if !isInitialized { | ||
| guard let apiKey = call.getString("apiKey") else { | ||
| throw GoogleMapErrors.invalidAPIKey | ||
| } | ||
| GMSServices.provideAPIKey(apiKey) | ||
| isInitialized = true | ||
| } | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let configObj = call.getObject("config") else { | ||
| throw GoogleMapErrors.invalidArguments("config object is missing") | ||
| } | ||
| let forceCreate = call.getBool("forceCreate", false) | ||
| let config = try GoogleMapConfig(fromJSObject: configObj) | ||
| if self.maps[id] != nil { | ||
| if !forceCreate { | ||
| call.resolve() | ||
| return | ||
| } | ||
| let removedMap = self.maps.removeValue(forKey: id) | ||
| removedMap?.destroy() | ||
| } | ||
| DispatchQueue.main.sync { | ||
| let newMap = Map(id: id, config: config, delegate: self) | ||
| self.maps[id] = newMap | ||
| } | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func destroy(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let removedMap = self.maps.removeValue(forKey: id) else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| removedMap.destroy() | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableTouch(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| map.enableTouch() | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func disableTouch(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| map.disableTouch() | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addTileOverlay(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let tileOverlayObj = call.getObject("tileOverlay") else { | ||
| throw GoogleMapErrors.invalidArguments("tileOverlay object is missing") | ||
| } | ||
| let tileOverlay = try TileOverlay(fromJSObject: tileOverlayObj) | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| let tileOverlayId = try map.addTileOverlay(tileOverlay: tileOverlay) | ||
| call.resolve(["id": String(tileOverlayId)]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removeTileOverlay(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let tileOverlayIdString = call.getString("tileOverlayId") else { | ||
| throw GoogleMapErrors.invalidArguments("tileOverlayId is invalid or missing") | ||
| } | ||
| guard let tileOverlayId = Int(tileOverlayIdString) else { | ||
| throw GoogleMapErrors.invalidArguments("tileOverlayId is invalid or missing") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removeTileOverlay(id: tileOverlayId) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addMarker(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let markerObj = call.getObject("marker") else { | ||
| throw GoogleMapErrors.invalidArguments("marker object is missing") | ||
| } | ||
| let marker = try Marker(fromJSObject: markerObj) | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| let markerId = try map.addMarker(marker: marker) | ||
| call.resolve(["id": String(markerId)]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addMarkers(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let markerObjs = call.getArray("markers") as? [JSObject] else { | ||
| throw GoogleMapErrors.invalidArguments("markers array is missing") | ||
| } | ||
| if markerObjs.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("markers requires at least one marker") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| var markers: [Marker] = [] | ||
| try markerObjs.forEach { marker in | ||
| let marker = try Marker(fromJSObject: marker) | ||
| markers.append(marker) | ||
| } | ||
| let ids = try map.addMarkers(markers: markers) | ||
| call.resolve(["ids": ids.map({ id in | ||
| return String(id) | ||
| })]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removeMarkers(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let markerIdStrings = call.getArray("markerIds") as? [String] else { | ||
| throw GoogleMapErrors.invalidArguments("markerIds are invalid or missing") | ||
| } | ||
| if markerIdStrings.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("markerIds requires at least one marker id") | ||
| } | ||
| let ids: [Int] = try markerIdStrings.map { idString in | ||
| guard let markerId = Int(idString) else { | ||
| throw GoogleMapErrors.invalidArguments("markerIds are invalid or missing") | ||
| } | ||
| return markerId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removeMarkers(ids: ids) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removeMarker(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let markerIdString = call.getString("markerId") else { | ||
| throw GoogleMapErrors.invalidArguments("markerId is invalid or missing") | ||
| } | ||
| guard let markerId = Int(markerIdString) else { | ||
| throw GoogleMapErrors.invalidArguments("markerId is invalid or missing") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removeMarker(id: markerId) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addPolygons(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let shapeObjs = call.getArray("polygons") as? [JSObject] else { | ||
| throw GoogleMapErrors.invalidArguments("polygons array is missing") | ||
| } | ||
| if shapeObjs.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("polygons requires at least one shape") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| var shapes: [Polygon] = [] | ||
| try shapeObjs.forEach { shapeObj in | ||
| let polygon = try Polygon(fromJSObject: shapeObj) | ||
| shapes.append(polygon) | ||
| } | ||
| let ids = try map.addPolygons(polygons: shapes) | ||
| call.resolve(["ids": ids.map({ id in | ||
| return String(id) | ||
| })]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addPolylines(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let lineObjs = call.getArray("polylines") as? [JSObject] else { | ||
| throw GoogleMapErrors.invalidArguments("polylines array is missing") | ||
| } | ||
| if lineObjs.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("polylines requires at least one line") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| var lines: [Polyline] = [] | ||
| try lineObjs.forEach { lineObj in | ||
| let line = try Polyline(fromJSObject: lineObj) | ||
| lines.append(line) | ||
| } | ||
| let ids = try map.addPolylines(lines: lines) | ||
| call.resolve(["ids": ids.map({ id in | ||
| return String(id) | ||
| })]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removePolygons(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let polygonIdsStrings = call.getArray("polygonIds") as? [String] else { | ||
| throw GoogleMapErrors.invalidArguments("polygonIds are invalid or missing") | ||
| } | ||
| if polygonIdsStrings.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("polygonIds requires at least one polygon id") | ||
| } | ||
| let ids: [Int] = try polygonIdsStrings.map { idString in | ||
| guard let polygonId = Int(idString) else { | ||
| throw GoogleMapErrors.invalidArguments("polygonIds are invalid or missing") | ||
| } | ||
| return polygonId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removePolygons(ids: ids) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addCircles(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let circleObjs = call.getArray("circles") as? [JSObject] else { | ||
| throw GoogleMapErrors.invalidArguments("circles array is missing") | ||
| } | ||
| if circleObjs.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("circles requires at least one circle") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| var circles: [Circle] = [] | ||
| try circleObjs.forEach { circleObj in | ||
| let circle = try Circle(from: circleObj) | ||
| circles.append(circle) | ||
| } | ||
| let ids = try map.addCircles(circles: circles) | ||
| call.resolve(["ids": ids.map({ id in | ||
| return String(id) | ||
| })]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removeCircles(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let circleIdsStrings = call.getArray("circleIds") as? [String] else { | ||
| throw GoogleMapErrors.invalidArguments("circleIds are invalid or missing") | ||
| } | ||
| if circleIdsStrings.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("circleIds requires at least one cicle id") | ||
| } | ||
| let ids: [Int] = try circleIdsStrings.map { idString in | ||
| guard let circleId = Int(idString) else { | ||
| throw GoogleMapErrors.invalidArguments("circleIds are invalid or missing") | ||
| } | ||
| return circleId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removeCircles(ids: ids) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removePolylines(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let polylineIdsStrings = call.getArray("polylineIds") as? [String] else { | ||
| throw GoogleMapErrors.invalidArguments("polylineIds are invalid or missing") | ||
| } | ||
| if polylineIdsStrings.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("polylineIds requires at least one polyline id") | ||
| } | ||
| let ids: [Int] = try polylineIdsStrings.map { idString in | ||
| guard let polylineId = Int(idString) else { | ||
| throw GoogleMapErrors.invalidArguments("polylineIds are invalid or missing") | ||
| } | ||
| return polylineId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removePolylines(ids: ids) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func setCamera(_ 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 configObj = call.getObject("config") else { | ||
| throw GoogleMapErrors.invalidArguments("config object is missing") | ||
| } | ||
| let config = try GoogleMapCameraConfig(fromJSObject: configObj) | ||
| try map.setCamera(config: config) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func getMapType(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| let mapType = GMSMapViewType.toString(mapType: map.getMapType()) | ||
| call.resolve([ | ||
| "type": mapType | ||
| ]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func setMapType(_ 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 mapTypeString = call.getString("mapType") else { | ||
| throw GoogleMapErrors.invalidArguments("mapType is missing") | ||
| } | ||
| let mapType = GMSMapViewType.fromString(mapType: mapTypeString) | ||
| try map.setMapType(mapType: mapType) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableIndoorMaps(_ 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 enabled = call.getBool("enabled") else { | ||
| throw GoogleMapErrors.invalidArguments("enabled is missing") | ||
| } | ||
| try map.enableIndoorMaps(enabled: enabled) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableTrafficLayer(_ 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 enabled = call.getBool("enabled") else { | ||
| throw GoogleMapErrors.invalidArguments("enabled is missing") | ||
| } | ||
| try map.enableTrafficLayer(enabled: enabled) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableAccessibilityElements(_ 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 enabled = call.getBool("enabled") else { | ||
| throw GoogleMapErrors.invalidArguments("enabled is missing") | ||
| } | ||
| try map.enableAccessibilityElements(enabled: enabled) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func setPadding(_ 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 configObj = call.getObject("padding") else { | ||
| throw GoogleMapErrors.invalidArguments("padding is missing") | ||
| } | ||
| let padding = try GoogleMapPadding.init(fromJSObject: configObj) | ||
| try map.setPadding(padding: padding) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableCurrentLocation(_ 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 enabled = call.getBool("enabled") else { | ||
| throw GoogleMapErrors.invalidArguments("enabled is missing") | ||
| } | ||
| let locationStatus = checkLocationPermission() | ||
| if enabled && !(locationStatus == "granted" || locationStatus == "prompt") { | ||
| throw GoogleMapErrors.permissionsDeniedLocation | ||
| } | ||
| try map.enableCurrentLocation(enabled: enabled) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableClustering(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| let minClusterSize = call.getInt("minClusterSize") | ||
| map.enableClustering(minClusterSize) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func disableClustering(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| map.disableClustering() | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func onScroll(_ call: CAPPluginCall) { | ||
| call.unavailable("not supported on iOS") | ||
| } | ||
| @objc func onResize(_ 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 mapBoundsObj = call.getObject("mapBounds") else { | ||
| throw GoogleMapErrors.invalidArguments("map bounds not set") | ||
| } | ||
| let mapBounds = try CGRect.fromJSObject(mapBoundsObj) | ||
| map.updateRender(mapBounds: mapBounds) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func onDisplay(_ 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 mapBoundsObj = call.getObject("mapBounds") else { | ||
| throw GoogleMapErrors.invalidArguments("map bounds not set") | ||
| } | ||
| let mapBounds = try CGRect.fromJSObject(mapBoundsObj) | ||
| map.rebindTargetContainer(mapBounds: mapBounds) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func getMapBounds(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try DispatchQueue.main.sync { | ||
| guard let bounds = map.getMapLatLngBounds() else { | ||
| throw GoogleMapErrors.unhandledError("Google Map Bounds could not be found.") | ||
| } | ||
| call.resolve( | ||
| formatMapBoundsForResponse( | ||
| bounds: bounds, | ||
| cameraPosition: map.mapViewController.GMapView.camera | ||
| ) | ||
| ) | ||
| } | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func mapBoundsContains(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let boundsObject = call.getObject("bounds") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid bounds provided") | ||
| } | ||
| guard let pointObject = call.getObject("point") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid point provided") | ||
| } | ||
| let bounds = try getGMSCoordinateBounds(boundsObject) | ||
| let point = try getCLLocationCoordinate(pointObject) | ||
| call.resolve([ | ||
| "contains": bounds.contains(point) | ||
| ]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func fitBounds(_ 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 boundsObject = call.getObject("bounds") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid bounds provided") | ||
| } | ||
| let bounds = try getGMSCoordinateBounds(boundsObject) | ||
| let padding = CGFloat(call.getInt("padding", 0)) | ||
| map.fitBounds(bounds: bounds, padding: padding) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func mapBoundsExtend(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let boundsObject = call.getObject("bounds") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid bounds provided") | ||
| } | ||
| guard let pointObject = call.getObject("point") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid point provided") | ||
| } | ||
| let bounds = try getGMSCoordinateBounds(boundsObject) | ||
| let point = try getCLLocationCoordinate(pointObject) | ||
| DispatchQueue.main.sync { | ||
| let newBounds = bounds.includingCoordinate(point) | ||
| call.resolve([ | ||
| "bounds": formatMapBoundsForResponse(newBounds) | ||
| ]) | ||
| } | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| private func getGMSCoordinateBounds(_ bounds: JSObject) throws -> GMSCoordinateBounds { | ||
| guard let southwest = bounds["southwest"] as? JSObject else { | ||
| throw GoogleMapErrors.unhandledError("Bounds southwest property not formatted properly.") | ||
| } | ||
| guard let northeast = bounds["northeast"] as? JSObject else { | ||
| throw GoogleMapErrors.unhandledError("Bounds northeast property not formatted properly.") | ||
| } | ||
| return GMSCoordinateBounds( | ||
| coordinate: try getCLLocationCoordinate(southwest), | ||
| coordinate: try getCLLocationCoordinate(northeast) | ||
| ) | ||
| } | ||
| private func getCLLocationCoordinate(_ point: JSObject) throws -> CLLocationCoordinate2D { | ||
| guard let lat = point["lat"] as? Double else { | ||
| throw GoogleMapErrors.unhandledError("Point lat property not formatted properly.") | ||
| } | ||
| guard let lng = point["lng"] as? Double else { | ||
| throw GoogleMapErrors.unhandledError("Point lng property not formatted properly.") | ||
| } | ||
| return CLLocationCoordinate2D(latitude: lat, longitude: lng) | ||
| } | ||
| private func formatMapBoundsForResponse(bounds: GMSCoordinateBounds?, cameraPosition: GMSCameraPosition) -> PluginCallResultData { | ||
| return [ | ||
| "southwest": [ | ||
| "lat": bounds?.southWest.latitude, | ||
| "lng": bounds?.southWest.longitude | ||
| ], | ||
| "center": [ | ||
| "lat": cameraPosition.target.latitude, | ||
| "lng": cameraPosition.target.longitude | ||
| ], | ||
| "northeast": [ | ||
| "lat": bounds?.northEast.latitude, | ||
| "lng": bounds?.northEast.longitude | ||
| ] | ||
| ] | ||
| } | ||
| private func formatMapBoundsForResponse(_ bounds: GMSCoordinateBounds) -> PluginCallResultData { | ||
| let centerLatitude = (bounds.southWest.latitude + bounds.northEast.latitude) / 2.0 | ||
| let centerLongitude = (bounds.southWest.longitude + bounds.northEast.longitude) / 2.0 | ||
| return [ | ||
| "southwest": [ | ||
| "lat": bounds.southWest.latitude, | ||
| "lng": bounds.southWest.longitude | ||
| ], | ||
| "center": [ | ||
| "lat": centerLatitude, | ||
| "lng": centerLongitude | ||
| ], | ||
| "northeast": [ | ||
| "lat": bounds.northEast.latitude, | ||
| "lng": bounds.northEast.longitude | ||
| ] | ||
| ] | ||
| } | ||
| private func handleError(_ call: CAPPluginCall, error: Error) { | ||
| let errObject = getErrorObject(error) | ||
| call.reject(errObject.message, "\(errObject.code)", error, [:]) | ||
| } | ||
| private func findMapIdByMapView(_ mapView: GMSMapView) -> String { | ||
| for (mapId, map) in self.maps { | ||
| if map.mapViewController.GMapView === mapView { | ||
| return mapId | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
| // --- EVENT LISTENERS --- | ||
| // onCameraIdle | ||
| public func mapView(_ mapView: GMSMapView, idleAt cameraPosition: GMSCameraPosition) { | ||
| let mapId = self.findMapIdByMapView(mapView) | ||
| let map = self.maps[mapId] | ||
| let bounds = map?.getMapLatLngBounds() | ||
| let data: PluginCallResultData = [ | ||
| "mapId": mapId, | ||
| "bounds": formatMapBoundsForResponse( | ||
| bounds: bounds, | ||
| cameraPosition: cameraPosition | ||
| ), | ||
| "bearing": cameraPosition.bearing, | ||
| "latitude": cameraPosition.target.latitude, | ||
| "longitude": cameraPosition.target.longitude, | ||
| "tilt": cameraPosition.viewingAngle, | ||
| "zoom": cameraPosition.zoom | ||
| ] | ||
| self.notifyListeners("onBoundsChanged", data: data) | ||
| self.notifyListeners("onCameraIdle", data: data) | ||
| } | ||
| // onCameraMoveStarted | ||
| public func mapView(_ mapView: GMSMapView, willMove gesture: Bool) { | ||
| self.notifyListeners("onCameraMoveStarted", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "isGesture": gesture | ||
| ]) | ||
| } | ||
| // onMapClick | ||
| public func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) { | ||
| self.notifyListeners("onMapClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "latitude": coordinate.latitude, | ||
| "longitude": coordinate.longitude | ||
| ]) | ||
| } | ||
| // onPolygonClick, onPolylineClick, onCircleClick | ||
| public func mapView(_ mapView: GMSMapView, didTap overlay: GMSOverlay) { | ||
| if let polygon = overlay as? GMSPolygon { | ||
| self.notifyListeners("onPolygonClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "polygonId": String(overlay.hash.hashValue), | ||
| "tag": polygon.userData as? String | ||
| ]) | ||
| } | ||
| if let circle = overlay as? GMSCircle { | ||
| self.notifyListeners("onCircleClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "circleId": String(overlay.hash.hashValue), | ||
| "tag": circle.userData as? String, | ||
| "latitude": circle.position.latitude, | ||
| "longitude": circle.position.longitude, | ||
| "radius": circle.radius | ||
| ]) | ||
| } | ||
| if let polyline = overlay as? GMSPolyline { | ||
| self.notifyListeners("onPolylineClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "polylineId": String(overlay.hash.hashValue), | ||
| "tag": polyline.userData as? String | ||
| ]) | ||
| } | ||
| } | ||
| // onClusterClick, onMarkerClick | ||
| public func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { | ||
| if let cluster = marker.userData as? GMUCluster { | ||
| var items: [[String: Any?]] = [] | ||
| for item in cluster.items { | ||
| items.append([ | ||
| "markerId": String(item.hash.hashValue), | ||
| "latitude": item.position.latitude, | ||
| "longitude": item.position.longitude, | ||
| "title": item.title ?? "", | ||
| "snippet": item.snippet ?? "" | ||
| ]) | ||
| } | ||
| self.notifyListeners("onClusterClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "latitude": cluster.position.latitude, | ||
| "longitude": cluster.position.longitude, | ||
| "size": cluster.count, | ||
| "items": items | ||
| ]) | ||
| } else { | ||
| self.notifyListeners("onMarkerClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| return false | ||
| } | ||
| // onMarkerDragStart | ||
| public func mapView(_ mapView: GMSMapView, didBeginDragging marker: GMSMarker) { | ||
| self.notifyListeners("onMarkerDragStart", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| // onMarkerDrag | ||
| public func mapView(_ mapView: GMSMapView, didDrag marker: GMSMarker) { | ||
| self.notifyListeners("onMarkerDrag", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| // onMarkerDragEnd | ||
| public func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) { | ||
| self.notifyListeners("onMarkerDragEnd", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| // onClusterInfoWindowClick, onInfoWindowClick | ||
| public func mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker) { | ||
| if let cluster = marker.userData as? GMUCluster { | ||
| var items: [[String: Any?]] = [] | ||
| for item in cluster.items { | ||
| items.append([ | ||
| "markerId": String(item.hash.hashValue), | ||
| "latitude": item.position.latitude, | ||
| "longitude": item.position.longitude, | ||
| "title": item.title ?? "", | ||
| "snippet": item.snippet ?? "" | ||
| ]) | ||
| } | ||
| self.notifyListeners("onClusterInfoWindowClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "latitude": cluster.position.latitude, | ||
| "longitude": cluster.position.longitude, | ||
| "size": cluster.count, | ||
| "items": items | ||
| ]) | ||
| } else { | ||
| self.notifyListeners("onInfoWindowClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| } | ||
| // onMyLocationButtonClick | ||
| public func didTapMyLocationButtonForMapView(for mapView: GMSMapView) -> Bool { | ||
| self.notifyListeners("onMyLocationButtonClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView) | ||
| ]) | ||
| return false | ||
| } | ||
| // onMyLocationClick | ||
| public func mapView(_ mapView: GMSMapView, didTapMyLocation location: CLLocationCoordinate2D) { | ||
| self.notifyListeners("onMyLocationButtonClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "latitude": location.latitude, | ||
| "longitude": location.longitude | ||
| ]) | ||
| } | ||
| } | ||
| // snippet from https://www.hackingwithswift.com/example-code/uicolor/how-to-convert-a-hex-color-to-a-uicolor | ||
| extension UIColor { | ||
| public convenience init?(hex: String) { | ||
| let r, g, b, a: CGFloat | ||
| if hex.hasPrefix("#") { | ||
| let start = hex.index(hex.startIndex, offsetBy: 1) | ||
| let hexColor = String(hex[start...]) | ||
| let scanner = Scanner(string: hexColor) | ||
| var hexNumber: UInt64 = 0 | ||
| if hexColor.count == 8 { | ||
| if scanner.scanHexInt64(&hexNumber) { | ||
| r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 | ||
| g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 | ||
| b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 | ||
| a = CGFloat(hexNumber & 0x000000ff) / 255 | ||
| self.init(red: r, green: g, blue: b, alpha: a) | ||
| return | ||
| } | ||
| } else { | ||
| if scanner.scanHexInt64(&hexNumber) { | ||
| r = CGFloat((hexNumber & 0xff0000) >> 16) / 255 | ||
| g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255 | ||
| b = CGFloat((hexNumber & 0x0000ff) >> 0) / 255 | ||
| self.init(red: r, green: g, blue: b, alpha: 1) | ||
| return | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct Circle { | ||
| let center: LatLng | ||
| let radius: Double | ||
| let strokeWidth: CGFloat | ||
| let strokeColor: UIColor | ||
| let fillColor: UIColor | ||
| let tappable: Bool? | ||
| let title: String? | ||
| let zIndex: Int32 | ||
| let tag: String? | ||
| init(from jsObject: JSObject) throws { | ||
| var strokeColor = UIColor.blue | ||
| var strokeWidth: CGFloat = 1.0 | ||
| var fillColor = UIColor.blue | ||
| guard let centerLatLng = jsObject["center"] as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("Circle object is missing the required 'center' property") | ||
| } | ||
| guard let lat = centerLatLng["lat"] as? Double, let lng = centerLatLng["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| guard let radius = jsObject["radius"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("Circle object is missing the required 'radius' property") | ||
| } | ||
| if let width = jsObject["strokeWeight"] as? Float { | ||
| strokeWidth = CGFloat(width) | ||
| } | ||
| let strokeOpacity = jsObject["strokeOpacity"] as? Double | ||
| if let hexColor = jsObject["strokeColor"] as? String { | ||
| strokeColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| strokeColor = strokeColor.withAlphaComponent(strokeOpacity ?? 1.0) | ||
| let fillOpacity = jsObject["fillOpacity"] as? Double | ||
| if let hexColor = jsObject["fillColor"] as? String { | ||
| fillColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| fillColor = fillColor.withAlphaComponent(fillOpacity ?? 1.0) | ||
| self.center = LatLng(lat: lat, lng: lng) | ||
| self.radius = radius | ||
| self.fillColor = fillColor | ||
| self.strokeColor = strokeColor | ||
| self.strokeWidth = strokeWidth | ||
| self.tag = jsObject["tag"] as? String | ||
| self.tappable = jsObject["clickable"] as? Bool | ||
| self.title = jsObject["title"] as? String | ||
| self.zIndex = Int32((jsObject["zIndex"] as? Int) ?? 0) | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct GoogleMapCameraConfig { | ||
| let coordinate: LatLng? | ||
| let zoom: Float? | ||
| let bearing: Double? | ||
| let angle: Double? | ||
| let animate: Bool? | ||
| let animationDuration: Double? | ||
| init(fromJSObject: JSObject) throws { | ||
| zoom = fromJSObject["zoom"] as? Float | ||
| bearing = fromJSObject["bearing"] as? Double | ||
| angle = fromJSObject["angle"] as? Double | ||
| animate = fromJSObject["animate"] as? Bool | ||
| animationDuration = fromJSObject["animationDuration"] as? Double | ||
| if let latLngObj = fromJSObject["coordinate"] as? JSObject { | ||
| guard let lat = latLngObj["lat"] as? Double, let lng = latLngObj["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| self.coordinate = LatLng(lat: lat, lng: lng) | ||
| } else { | ||
| self.coordinate = nil | ||
| } | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| import GoogleMaps | ||
| public struct GoogleMapConfig: Codable { | ||
| let width: Double | ||
| let height: Double | ||
| let x: Double | ||
| let y: Double | ||
| let center: LatLng | ||
| let zoom: Double | ||
| let styles: String? | ||
| var mapId: String? | ||
| let mapTypeId: String? | ||
| let maxZoom: Double? | ||
| let minZoom: Double? | ||
| let restriction: GoogleMapConfigRestriction? | ||
| let heading: Double? | ||
| init(fromJSObject: JSObject) throws { | ||
| guard let width = fromJSObject["width"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'width' property") | ||
| } | ||
| guard let height = fromJSObject["height"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'height' property") | ||
| } | ||
| guard let x = fromJSObject["x"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'x' property") | ||
| } | ||
| guard let y = fromJSObject["y"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'y' property") | ||
| } | ||
| guard let zoom = fromJSObject["zoom"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'zoom' property") | ||
| } | ||
| guard let latLngObj = fromJSObject["center"] as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'center' property") | ||
| } | ||
| guard let lat = latLngObj["lat"] as? Double, let lng = latLngObj["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| self.width = round(width) | ||
| self.height = round(height) | ||
| self.x = x | ||
| self.y = y | ||
| self.center = LatLng(lat: lat, lng: lng) | ||
| if let stylesArray = fromJSObject["styles"] as? JSArray, let jsonData = try? JSONSerialization.data(withJSONObject: stylesArray, options: []) { | ||
| self.styles = String(data: jsonData, encoding: .utf8) | ||
| } else { | ||
| self.styles = nil | ||
| } | ||
| self.mapId = fromJSObject["iOSMapId"] as? String | ||
| self.mapTypeId = fromJSObject["mapTypeId"] as? String | ||
| var maxZoom = fromJSObject["maxZoom"] as? Double | ||
| var minZoom = fromJSObject["minZoom"] as? Double | ||
| if let unwrappedMinZoom = minZoom, let unwrappedMaxZoom = maxZoom, unwrappedMinZoom > unwrappedMaxZoom { | ||
| swap(&minZoom, &maxZoom) | ||
| } | ||
| self.minZoom = minZoom | ||
| self.maxZoom = maxZoom | ||
| if let maxZoom, zoom > maxZoom { | ||
| self.zoom = maxZoom | ||
| } else if let minZoom, zoom < minZoom { | ||
| self.zoom = minZoom | ||
| } else { | ||
| self.zoom = zoom | ||
| } | ||
| if let restrictionObj = fromJSObject["restriction"] as? JSObject { | ||
| self.restriction = try GoogleMapConfigRestriction(fromJSObject: restrictionObj) | ||
| } else { | ||
| self.restriction = nil | ||
| } | ||
| self.heading = fromJSObject["heading"] as? Double | ||
| } | ||
| } | ||
| public struct GoogleMapConfigRestriction: Codable { | ||
| let latLngBounds: GMSCoordinateBounds | ||
| init(fromJSObject: JSObject) throws { | ||
| guard let latLngBoundsObj = fromJSObject["latLngBounds"] as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds' property") | ||
| } | ||
| guard let north = latLngBoundsObj["north"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds.north' property") | ||
| } | ||
| guard let south = latLngBoundsObj["south"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds.south' property") | ||
| } | ||
| guard let east = latLngBoundsObj["east"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds.east' property") | ||
| } | ||
| guard let west = latLngBoundsObj["west"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds.west' property") | ||
| } | ||
| let southWest = CLLocationCoordinate2D(latitude: south, longitude: west) | ||
| let northEast = CLLocationCoordinate2D(latitude: north, longitude: east) | ||
| self.latLngBounds = GMSCoordinateBounds(coordinate: southWest, coordinate: northEast) | ||
| } | ||
| enum CodingKeys: String, CodingKey { | ||
| case latLngBounds | ||
| } | ||
| struct LatLngBounds: Codable { | ||
| let north: CLLocationDegrees | ||
| let south: CLLocationDegrees | ||
| let east: CLLocationDegrees | ||
| let west: CLLocationDegrees | ||
| init(north: CLLocationDegrees, south: CLLocationDegrees, east: CLLocationDegrees, west: CLLocationDegrees) { | ||
| self.north = north | ||
| self.south = south | ||
| self.east = east | ||
| self.west = west | ||
| } | ||
| } | ||
| public func encode(to encoder: any Encoder) throws { | ||
| var container = encoder.container(keyedBy: CodingKeys.self) | ||
| let latLngBounds = LatLngBounds( | ||
| north: self.latLngBounds.northEast.latitude, | ||
| south: self.latLngBounds.southWest.latitude, | ||
| east: self.latLngBounds.northEast.longitude, | ||
| west: self.latLngBounds.southWest.longitude | ||
| ) | ||
| try container.encode(latLngBounds, forKey: .latLngBounds) | ||
| } | ||
| public init(from decoder: any Decoder) throws { | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| let latLngBounds = try container.decode(LatLngBounds.self, forKey: .latLngBounds) | ||
| let southWest = CLLocationCoordinate2D(latitude: latLngBounds.south, longitude: latLngBounds.west) | ||
| let northEast = CLLocationCoordinate2D(latitude: latLngBounds.north, longitude: latLngBounds.east) | ||
| self.latLngBounds = GMSCoordinateBounds(coordinate: southWest, coordinate: northEast) | ||
| } | ||
| } |
| import Foundation | ||
| public enum GoogleMapErrors: Error { | ||
| case invalidMapId | ||
| case mapNotFound | ||
| case markerNotFound | ||
| case invalidArguments(_ description: String) | ||
| case invalidAPIKey | ||
| case permissionsDeniedLocation | ||
| case unhandledError(_ description: String) | ||
| case tileOverlayNotFound | ||
| } | ||
| public struct GoogleMapErrorObject { | ||
| let extra: [String: Any]? | ||
| let code: Int | ||
| let message: String | ||
| init(_ code: Int, _ message: String, _ extra: [String: Any]? = nil) { | ||
| self.code = code | ||
| self.message = message | ||
| self.extra = extra | ||
| } | ||
| var asDictionary: [String: Any] { | ||
| return ["code": code, "message": message, "extra": extra ?? []] | ||
| } | ||
| } | ||
| public func getErrorObject(_ error: Error) -> GoogleMapErrorObject { | ||
| switch error { | ||
| case GoogleMapErrors.invalidMapId: | ||
| return GoogleMapErrorObject(1, "Missing or invalid map id.") | ||
| case GoogleMapErrors.mapNotFound: | ||
| return GoogleMapErrorObject(2, "Map not found for provided id.") | ||
| case GoogleMapErrors.markerNotFound: | ||
| return GoogleMapErrorObject(3, "Marker not found for provided id.") | ||
| case GoogleMapErrors.invalidArguments(let msg): | ||
| return GoogleMapErrorObject(4, "Invalid Arguments Provided: \(msg)") | ||
| case GoogleMapErrors.permissionsDeniedLocation: | ||
| return GoogleMapErrorObject(5, "Permissions denied for accessing device location.") | ||
| case GoogleMapErrors.invalidAPIKey: | ||
| return GoogleMapErrorObject(6, "Missing or invalid Google Maps SDK API key.") | ||
| case GoogleMapErrors.tileOverlayNotFound: | ||
| return GoogleMapErrorObject(7, "Tile overlay not found for provided id.") | ||
| case GoogleMapErrors.unhandledError(let msg): | ||
| return GoogleMapErrorObject(0, "Unhandled Error: \(msg)") | ||
| default: | ||
| return GoogleMapErrorObject(0, "Unhandled Error: \(error.localizedDescription)") | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct GoogleMapPadding { | ||
| let top: Float | ||
| let bottom: Float | ||
| let left: Float | ||
| let right: Float | ||
| init(fromJSObject: JSObject) throws { | ||
| top = fromJSObject["top"] as? Float ?? 0 | ||
| bottom = fromJSObject["bottom"] as? Float ?? 0 | ||
| left = fromJSObject["left"] as? Float ?? 0 | ||
| right = fromJSObject["right"] as? Float ?? 0 | ||
| } | ||
| } |
| import Foundation | ||
| import GoogleMaps | ||
| import Capacitor | ||
| import GoogleMapsUtils | ||
| public struct LatLng: Codable { | ||
| let lat: Double | ||
| let lng: Double | ||
| } | ||
| class GMViewController: UIViewController { | ||
| var mapViewBounds: [String: Double]! | ||
| var GMapView: GMSMapView! | ||
| var cameraPosition: [String: Double]! | ||
| var minimumClusterSize: Int? | ||
| var mapId: String? | ||
| private var clusterManager: GMUClusterManager? | ||
| var clusteringEnabled: Bool { | ||
| return clusterManager != nil | ||
| } | ||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
| let camera = GMSCameraPosition.camera(withLatitude: cameraPosition["latitude"] ?? 0, longitude: cameraPosition["longitude"] ?? 0, zoom: Float(cameraPosition["zoom"] ?? 12)) | ||
| let frame = CGRect(x: mapViewBounds["x"] ?? 0, y: mapViewBounds["y"] ?? 0, width: mapViewBounds["width"] ?? 0, height: mapViewBounds["height"] ?? 0) | ||
| if let id = mapId { | ||
| let gmsId = GMSMapID(identifier: id) | ||
| self.GMapView = GMSMapView(frame: frame, mapID: gmsId, camera: camera) | ||
| } else { | ||
| self.GMapView = GMSMapView(frame: frame, camera: camera) | ||
| } | ||
| self.view = GMapView | ||
| } | ||
| func initClusterManager(_ minClusterSize: Int?) { | ||
| let iconGenerator = GMUDefaultClusterIconGenerator() | ||
| let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm() | ||
| let renderer = GMUDefaultClusterRenderer(mapView: self.GMapView, clusterIconGenerator: iconGenerator) | ||
| self.minimumClusterSize = minClusterSize | ||
| if let minClusterSize = minClusterSize { | ||
| renderer.minimumClusterSize = UInt(minClusterSize) | ||
| } | ||
| self.clusterManager = GMUClusterManager(map: self.GMapView, algorithm: algorithm, renderer: renderer) | ||
| } | ||
| func destroyClusterManager() { | ||
| self.clusterManager = nil | ||
| } | ||
| func addMarkersToCluster(markers: [GMSMarker]) { | ||
| if let clusterManager = clusterManager { | ||
| clusterManager.add(markers) | ||
| clusterManager.cluster() | ||
| } | ||
| } | ||
| func removeMarkersFromCluster(markers: [GMSMarker]) { | ||
| if let clusterManager = clusterManager { | ||
| markers.forEach { marker in | ||
| clusterManager.remove(marker) | ||
| } | ||
| clusterManager.cluster() | ||
| } | ||
| } | ||
| } | ||
| // swiftlint:disable type_body_length | ||
| public class Map { | ||
| var id: String | ||
| var config: GoogleMapConfig | ||
| var mapViewController: GMViewController | ||
| var targetViewController: UIView? | ||
| var markers = [Int: GMSMarker]() | ||
| var tileOverlays = [Int: GMSURLTileLayer]() | ||
| var polygons = [Int: GMSPolygon]() | ||
| var circles = [Int: GMSCircle]() | ||
| var polylines = [Int: GMSPolyline]() | ||
| var markerIcons = [String: UIImage]() | ||
| // swiftlint:disable identifier_name | ||
| public static let MAP_TAG = 99999 | ||
| // swiftlint:enable identifier_name | ||
| // swiftlint:disable weak_delegate | ||
| private var delegate: CapacitorGoogleMapsPlugin | ||
| init(id: String, config: GoogleMapConfig, delegate: CapacitorGoogleMapsPlugin) { | ||
| self.id = id | ||
| self.config = config | ||
| self.delegate = delegate | ||
| self.mapViewController = GMViewController() | ||
| self.mapViewController.mapId = config.mapId | ||
| self.render() | ||
| } | ||
| func render() { | ||
| DispatchQueue.main.async { | ||
| self.mapViewController.mapViewBounds = [ | ||
| "width": self.config.width, | ||
| "height": self.config.height, | ||
| "x": self.config.x, | ||
| "y": self.config.y | ||
| ] | ||
| self.mapViewController.cameraPosition = [ | ||
| "latitude": self.config.center.lat, | ||
| "longitude": self.config.center.lng, | ||
| "zoom": self.config.zoom | ||
| ] | ||
| self.targetViewController = self.getTargetContainer(refWidth: self.config.width, refHeight: self.config.height) | ||
| if let target = self.targetViewController { | ||
| target.tag = Map.MAP_TAG | ||
| target.removeAllSubview() | ||
| self.mapViewController.view.frame = target.bounds | ||
| target.addSubview(self.mapViewController.view) | ||
| self.mapViewController.GMapView.delegate = self.delegate | ||
| } | ||
| if let styles = self.config.styles { | ||
| do { | ||
| self.mapViewController.GMapView.mapStyle = try GMSMapStyle(jsonString: styles) | ||
| } catch { | ||
| CAPLog.print("Invalid Google Maps styles") | ||
| } | ||
| } | ||
| let minZoom = self.config.minZoom.map { Float($0) } ?? self.mapViewController.GMapView.minZoom | ||
| let maxZoom = self.config.maxZoom.map { Float($0) } ?? self.mapViewController.GMapView.maxZoom | ||
| self.mapViewController.GMapView.setMinZoom(minZoom, maxZoom: maxZoom) | ||
| if let mapTypeId = self.config.mapTypeId { | ||
| switch mapTypeId { | ||
| case "hybrid": | ||
| self.mapViewController.GMapView.mapType = .hybrid | ||
| case "roadmap": | ||
| self.mapViewController.GMapView.mapType = .normal | ||
| case "satellite": | ||
| self.mapViewController.GMapView.mapType = .satellite | ||
| case "terrain": | ||
| self.mapViewController.GMapView.mapType = .terrain | ||
| default: | ||
| break | ||
| } | ||
| } | ||
| if let restriction = self.config.restriction { | ||
| self.mapViewController.GMapView.cameraTargetBounds = restriction.latLngBounds | ||
| } | ||
| if let heading = self.config.heading { | ||
| self.mapViewController.GMapView.animate(toBearing: heading) | ||
| } | ||
| self.delegate.notifyListeners("onMapReady", data: [ | ||
| "mapId": self.id | ||
| ]) | ||
| } | ||
| } | ||
| func updateRender(mapBounds: CGRect) { | ||
| DispatchQueue.main.sync { | ||
| let newWidth = round(Double(mapBounds.width)) | ||
| let newHeight = round(Double(mapBounds.height)) | ||
| let isWidthEqual = round(Double(self.mapViewController.view.bounds.width)) == newWidth | ||
| let isHeightEqual = round(Double(self.mapViewController.view.bounds.height)) == newHeight | ||
| if !isWidthEqual || !isHeightEqual { | ||
| CATransaction.begin() | ||
| CATransaction.setDisableActions(true) | ||
| self.mapViewController.view.frame.size.width = newWidth | ||
| self.mapViewController.view.frame.size.height = newHeight | ||
| CATransaction.commit() | ||
| } | ||
| } | ||
| } | ||
| func rebindTargetContainer(mapBounds: CGRect) { | ||
| DispatchQueue.main.sync { | ||
| if let target = self.getTargetContainer(refWidth: round(Double(mapBounds.width)), refHeight: round(Double(mapBounds.height))) { | ||
| self.targetViewController = target | ||
| target.tag = Map.MAP_TAG | ||
| target.removeAllSubview() | ||
| CATransaction.begin() | ||
| CATransaction.setDisableActions(true) | ||
| self.mapViewController.view.frame.size.width = mapBounds.width | ||
| self.mapViewController.view.frame.size.height = mapBounds.height | ||
| CATransaction.commit() | ||
| target.addSubview(self.mapViewController.view) | ||
| } | ||
| } | ||
| } | ||
| private func getTargetContainer(refWidth: Double, refHeight: Double) -> UIView? { | ||
| if let bridge = self.delegate.bridge { | ||
| for item in bridge.webView!.getAllSubViews() { | ||
| let isScrollView = item.isKind(of: NSClassFromString("WKChildScrollView")!) || item.isKind(of: NSClassFromString("WKScrollView")!) | ||
| let isBridgeScrollView = item.isEqual(bridge.webView?.scrollView) | ||
| if isScrollView && !isBridgeScrollView { | ||
| (item as? UIScrollView)?.isScrollEnabled = true | ||
| let height = Double((item as? UIScrollView)?.contentSize.height ?? 0) | ||
| let width = Double((item as? UIScrollView)?.contentSize.width ?? 0) | ||
| let actualHeight = round(height / 2) | ||
| let isWidthEqual = width == self.config.width | ||
| let isHeightEqual = actualHeight == self.config.height | ||
| if isWidthEqual && isHeightEqual && item.tag < self.targetViewController?.tag ?? Map.MAP_TAG { | ||
| return item | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| func destroy() { | ||
| DispatchQueue.main.async { | ||
| self.mapViewController.GMapView = nil | ||
| self.targetViewController?.tag = 0 | ||
| self.mapViewController.view = nil | ||
| self.enableTouch() | ||
| } | ||
| } | ||
| func enableTouch() { | ||
| DispatchQueue.main.async { | ||
| if let target = self.targetViewController, let itemIndex = WKWebView.disabledTargets.firstIndex(of: target) { | ||
| WKWebView.disabledTargets.remove(at: itemIndex) | ||
| } | ||
| } | ||
| } | ||
| func addTileOverlay(tileOverlay: TileOverlay) throws -> Int { | ||
| var tileOverlayHash = 0 | ||
| DispatchQueue.main.sync { | ||
| let urlConstructor: GMSTileURLConstructor = { x, y, zoom in | ||
| URL(string: tileOverlay.url | ||
| .replacingOccurrences(of: "{x}", with: "\(x)") | ||
| .replacingOccurrences(of: "{y}", with: "\(y)") | ||
| .replacingOccurrences(of: "{z}", with: "\(zoom)") | ||
| ) | ||
| } | ||
| let layer = GMSURLTileLayer(urlConstructor: urlConstructor) | ||
| layer.opacity = tileOverlay.opacity ?? 1 | ||
| layer.zIndex = tileOverlay.zIndex | ||
| layer.map = self.mapViewController.GMapView | ||
| self.tileOverlays[layer.hash.hashValue] = layer | ||
| tileOverlayHash = layer.hash.hashValue | ||
| } | ||
| return tileOverlayHash | ||
| } | ||
| func removeTileOverlay(id: Int) throws { | ||
| if let tileOverlay = self.tileOverlays[id] { | ||
| DispatchQueue.main.async { | ||
| tileOverlay.map = nil | ||
| self.tileOverlays.removeValue(forKey: id) | ||
| } | ||
| } else { | ||
| throw GoogleMapErrors.tileOverlayNotFound | ||
| } | ||
| } | ||
| func disableTouch() { | ||
| DispatchQueue.main.async { | ||
| if let target = self.targetViewController, !WKWebView.disabledTargets.contains(target) { | ||
| WKWebView.disabledTargets.append(target) | ||
| } | ||
| } | ||
| } | ||
| func addMarker(marker: Marker) throws -> Int { | ||
| var markerHash = 0 | ||
| DispatchQueue.main.sync { | ||
| let newMarker = self.buildMarker(marker: marker) | ||
| if self.mapViewController.clusteringEnabled { | ||
| self.mapViewController.addMarkersToCluster(markers: [newMarker]) | ||
| } else { | ||
| newMarker.map = self.mapViewController.GMapView | ||
| } | ||
| self.markers[newMarker.hash.hashValue] = newMarker | ||
| markerHash = newMarker.hash.hashValue | ||
| } | ||
| return markerHash | ||
| } | ||
| func addMarkers(markers: [Marker]) throws -> [Int] { | ||
| var markerHashes: [Int] = [] | ||
| DispatchQueue.main.sync { | ||
| var googleMapsMarkers: [GMSMarker] = [] | ||
| markers.forEach { marker in | ||
| let newMarker = self.buildMarker(marker: marker) | ||
| if self.mapViewController.clusteringEnabled { | ||
| googleMapsMarkers.append(newMarker) | ||
| } else { | ||
| newMarker.map = self.mapViewController.GMapView | ||
| } | ||
| self.markers[newMarker.hash.hashValue] = newMarker | ||
| markerHashes.append(newMarker.hash.hashValue) | ||
| } | ||
| if self.mapViewController.clusteringEnabled { | ||
| self.mapViewController.addMarkersToCluster(markers: googleMapsMarkers) | ||
| } | ||
| } | ||
| return markerHashes | ||
| } | ||
| func addPolygons(polygons: [Polygon]) throws -> [Int] { | ||
| var polygonHashes: [Int] = [] | ||
| DispatchQueue.main.sync { | ||
| polygons.forEach { polygon in | ||
| let newPolygon = self.buildPolygon(polygon: polygon) | ||
| newPolygon.map = self.mapViewController.GMapView | ||
| self.polygons[newPolygon.hash.hashValue] = newPolygon | ||
| polygonHashes.append(newPolygon.hash.hashValue) | ||
| } | ||
| } | ||
| return polygonHashes | ||
| } | ||
| func addCircles(circles: [Circle]) throws -> [Int] { | ||
| var circleHashes: [Int] = [] | ||
| DispatchQueue.main.sync { | ||
| circles.forEach { circle in | ||
| let newCircle = self.buildCircle(circle: circle) | ||
| newCircle.map = self.mapViewController.GMapView | ||
| self.circles[newCircle.hash.hashValue] = newCircle | ||
| circleHashes.append(newCircle.hash.hashValue) | ||
| } | ||
| } | ||
| return circleHashes | ||
| } | ||
| func addPolylines(lines: [Polyline]) throws -> [Int] { | ||
| var polylineHashes: [Int] = [] | ||
| DispatchQueue.main.sync { | ||
| lines.forEach { line in | ||
| let newLine = self.buildPolyline(line: line) | ||
| newLine.map = self.mapViewController.GMapView | ||
| self.polylines[newLine.hash.hashValue] = newLine | ||
| polylineHashes.append(newLine.hash.hashValue) | ||
| } | ||
| } | ||
| return polylineHashes | ||
| } | ||
| func enableClustering(_ minClusterSize: Int?) { | ||
| if !self.mapViewController.clusteringEnabled { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.initClusterManager(minClusterSize) | ||
| // add existing markers to the cluster | ||
| if !self.markers.isEmpty { | ||
| var existingMarkers: [GMSMarker] = [] | ||
| for (_, marker) in self.markers { | ||
| marker.map = nil | ||
| existingMarkers.append(marker) | ||
| } | ||
| self.mapViewController.addMarkersToCluster(markers: existingMarkers) | ||
| } | ||
| } | ||
| } else if self.mapViewController.minimumClusterSize != minClusterSize { | ||
| self.mapViewController.destroyClusterManager() | ||
| enableClustering(minClusterSize) | ||
| } | ||
| } | ||
| func disableClustering() { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.destroyClusterManager() | ||
| // add existing markers back to the map | ||
| if !self.markers.isEmpty { | ||
| for (_, marker) in self.markers { | ||
| marker.map = self.mapViewController.GMapView | ||
| } | ||
| } | ||
| } | ||
| } | ||
| func removeMarker(id: Int) throws { | ||
| if let marker = self.markers[id] { | ||
| DispatchQueue.main.async { | ||
| if self.mapViewController.clusteringEnabled { | ||
| self.mapViewController.removeMarkersFromCluster(markers: [marker]) | ||
| } | ||
| marker.map = nil | ||
| self.markers.removeValue(forKey: id) | ||
| } | ||
| } else { | ||
| throw GoogleMapErrors.markerNotFound | ||
| } | ||
| } | ||
| func removePolygons(ids: [Int]) throws { | ||
| DispatchQueue.main.sync { | ||
| ids.forEach { id in | ||
| if let polygon = self.polygons[id] { | ||
| polygon.map = nil | ||
| self.polygons.removeValue(forKey: id) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| func removeCircles(ids: [Int]) throws { | ||
| DispatchQueue.main.sync { | ||
| ids.forEach { id in | ||
| if let circle = self.circles[id] { | ||
| circle.map = nil | ||
| self.circles.removeValue(forKey: id) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| func removePolylines(ids: [Int]) throws { | ||
| DispatchQueue.main.sync { | ||
| ids.forEach { id in | ||
| if let line = self.polylines[id] { | ||
| line.map = nil | ||
| self.polylines.removeValue(forKey: id) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| func setCamera(config: GoogleMapCameraConfig) throws { | ||
| let currentCamera = self.mapViewController.GMapView.camera | ||
| let lat = config.coordinate?.lat ?? currentCamera.target.latitude | ||
| let lng = config.coordinate?.lng ?? currentCamera.target.longitude | ||
| let zoom = config.zoom ?? currentCamera.zoom | ||
| let bearing = config.bearing ?? Double(currentCamera.bearing) | ||
| let angle = config.angle ?? currentCamera.viewingAngle | ||
| let animate = config.animate ?? false | ||
| DispatchQueue.main.sync { | ||
| let newCamera = GMSCameraPosition(latitude: lat, longitude: lng, zoom: zoom, bearing: bearing, viewingAngle: angle) | ||
| if animate { | ||
| self.mapViewController.GMapView.animate(to: newCamera) | ||
| } else { | ||
| self.mapViewController.GMapView.camera = newCamera | ||
| } | ||
| } | ||
| } | ||
| func getMapType() -> GMSMapViewType { | ||
| return self.mapViewController.GMapView.mapType | ||
| } | ||
| func setMapType(mapType: GMSMapViewType) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.mapType = mapType | ||
| } | ||
| } | ||
| func enableIndoorMaps(enabled: Bool) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.isIndoorEnabled = enabled | ||
| } | ||
| } | ||
| func enableTrafficLayer(enabled: Bool) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.isTrafficEnabled = enabled | ||
| } | ||
| } | ||
| func enableAccessibilityElements(enabled: Bool) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.accessibilityElementsHidden = enabled | ||
| } | ||
| } | ||
| func enableCurrentLocation(enabled: Bool) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.isMyLocationEnabled = enabled | ||
| } | ||
| } | ||
| func setPadding(padding: GoogleMapPadding) throws { | ||
| DispatchQueue.main.sync { | ||
| let mapInsets = UIEdgeInsets(top: CGFloat(padding.top), left: CGFloat(padding.left), bottom: CGFloat(padding.bottom), right: CGFloat(padding.right)) | ||
| self.mapViewController.GMapView.padding = mapInsets | ||
| } | ||
| } | ||
| func removeMarkers(ids: [Int]) throws { | ||
| DispatchQueue.main.sync { | ||
| var markers: [GMSMarker] = [] | ||
| ids.forEach { id in | ||
| if let marker = self.markers[id] { | ||
| marker.map = nil | ||
| self.markers.removeValue(forKey: id) | ||
| markers.append(marker) | ||
| } | ||
| } | ||
| if self.mapViewController.clusteringEnabled { | ||
| self.mapViewController.removeMarkersFromCluster(markers: markers) | ||
| } | ||
| } | ||
| } | ||
| func getMapLatLngBounds() -> GMSCoordinateBounds? { | ||
| return GMSCoordinateBounds(region: self.mapViewController.GMapView.projection.visibleRegion()) | ||
| } | ||
| func fitBounds(bounds: GMSCoordinateBounds, padding: CGFloat) { | ||
| DispatchQueue.main.sync { | ||
| let cameraUpdate = GMSCameraUpdate.fit(bounds, withPadding: padding) | ||
| self.mapViewController.GMapView.animate(with: cameraUpdate) | ||
| } | ||
| } | ||
| private func getFrameOverflowBounds(frame: CGRect, mapBounds: CGRect) -> [CGRect] { | ||
| var intersections: [CGRect] = [] | ||
| // get top overflow | ||
| if mapBounds.origin.y < frame.origin.y { | ||
| let height = frame.origin.y - mapBounds.origin.y | ||
| let width = mapBounds.width | ||
| intersections.append(CGRect(x: 0, y: 0, width: width, height: height)) | ||
| } | ||
| // get bottom overflow | ||
| if (mapBounds.origin.y + mapBounds.height) > (frame.origin.y + frame.height) { | ||
| let height = (mapBounds.origin.y + mapBounds.height) - (frame.origin.y + frame.height) | ||
| let width = mapBounds.width | ||
| intersections.append(CGRect(x: 0, y: mapBounds.height, width: width, height: height)) | ||
| } | ||
| return intersections | ||
| } | ||
| private func buildCircle(circle: Circle) -> GMSCircle { | ||
| let newCircle = GMSCircle() | ||
| newCircle.title = circle.title | ||
| newCircle.strokeColor = circle.strokeColor | ||
| newCircle.strokeWidth = circle.strokeWidth | ||
| newCircle.fillColor = circle.fillColor | ||
| newCircle.position = CLLocationCoordinate2D(latitude: circle.center.lat, longitude: circle.center.lng) | ||
| newCircle.radius = CLLocationDistance(circle.radius) | ||
| newCircle.isTappable = circle.tappable ?? false | ||
| newCircle.zIndex = circle.zIndex | ||
| newCircle.userData = circle.tag | ||
| return newCircle | ||
| } | ||
| private func buildPolygon(polygon: Polygon) -> GMSPolygon { | ||
| let newPolygon = GMSPolygon() | ||
| newPolygon.title = polygon.title | ||
| newPolygon.strokeColor = polygon.strokeColor | ||
| newPolygon.strokeWidth = polygon.strokeWidth | ||
| newPolygon.fillColor = polygon.fillColor | ||
| newPolygon.isTappable = polygon.tappable ?? false | ||
| newPolygon.geodesic = polygon.geodesic ?? false | ||
| newPolygon.zIndex = polygon.zIndex | ||
| newPolygon.userData = polygon.tag | ||
| var shapeIndex = 0 | ||
| let outerShape = GMSMutablePath() | ||
| var holes: [GMSMutablePath] = [] | ||
| polygon.shapes.forEach { shape in | ||
| if shapeIndex == 0 { | ||
| shape.forEach { coord in | ||
| outerShape.add(CLLocationCoordinate2D(latitude: coord.lat, longitude: coord.lng)) | ||
| } | ||
| } else { | ||
| let holeShape = GMSMutablePath() | ||
| shape.forEach { coord in | ||
| holeShape.add(CLLocationCoordinate2D(latitude: coord.lat, longitude: coord.lng)) | ||
| } | ||
| holes.append(holeShape) | ||
| } | ||
| shapeIndex += 1 | ||
| } | ||
| newPolygon.path = outerShape | ||
| newPolygon.holes = holes | ||
| return newPolygon | ||
| } | ||
| private func buildPolyline(line: Polyline) -> GMSPolyline { | ||
| let newPolyline = GMSPolyline() | ||
| newPolyline.title = line.title | ||
| newPolyline.strokeColor = line.strokeColor | ||
| newPolyline.strokeWidth = line.strokeWidth | ||
| newPolyline.isTappable = line.tappable ?? false | ||
| newPolyline.geodesic = line.geodesic ?? false | ||
| newPolyline.zIndex = line.zIndex | ||
| newPolyline.userData = line.tag | ||
| let path = GMSMutablePath() | ||
| line.path.forEach { coord in | ||
| path.add(CLLocationCoordinate2D(latitude: coord.lat, longitude: coord.lng)) | ||
| } | ||
| newPolyline.path = path | ||
| if line.styleSpans.count > 0 { | ||
| var spans: [GMSStyleSpan] = [] | ||
| line.styleSpans.forEach { span in | ||
| if let segments = span.segments { | ||
| spans.append(GMSStyleSpan(color: span.color, segments: segments)) | ||
| } else { | ||
| spans.append(GMSStyleSpan(color: span.color)) | ||
| } | ||
| } | ||
| newPolyline.spans = spans | ||
| } | ||
| return newPolyline | ||
| } | ||
| private func buildMarker(marker: Marker) -> GMSMarker { | ||
| let newMarker = GMSMarker() | ||
| newMarker.position = CLLocationCoordinate2D(latitude: marker.coordinate.lat, longitude: marker.coordinate.lng) | ||
| newMarker.title = marker.title | ||
| newMarker.snippet = marker.snippet | ||
| newMarker.isFlat = marker.isFlat ?? false | ||
| newMarker.opacity = marker.opacity ?? 1 | ||
| newMarker.isDraggable = marker.draggable ?? false | ||
| newMarker.zIndex = marker.zIndex | ||
| if let iconAnchor = marker.iconAnchor { | ||
| newMarker.groundAnchor = iconAnchor | ||
| } | ||
| // cache and reuse marker icon uiimages | ||
| if let iconUrl = marker.iconUrl { | ||
| if let iconImage = self.markerIcons[iconUrl] { | ||
| newMarker.icon = getResizedIcon(iconImage, marker) | ||
| } else { | ||
| if iconUrl.starts(with: "https:") { | ||
| if let url = URL(string: iconUrl) { | ||
| URLSession.shared.dataTask(with: url) { (data, _, _) in | ||
| DispatchQueue.main.async { | ||
| if let data = data, let iconImage = UIImage(data: data) { | ||
| self.markerIcons[iconUrl] = iconImage | ||
| newMarker.icon = getResizedIcon(iconImage, marker) | ||
| } | ||
| } | ||
| }.resume() | ||
| } | ||
| } else if let iconImage = UIImage(named: "public/\(iconUrl)") { | ||
| self.markerIcons[iconUrl] = iconImage | ||
| newMarker.icon = getResizedIcon(iconImage, marker) | ||
| } else { | ||
| var detailedMessage = "" | ||
| if iconUrl.hasSuffix(".svg") { | ||
| detailedMessage = "SVG not supported." | ||
| } | ||
| print("CapacitorGoogleMaps Warning: could not load image '\(iconUrl)'. \(detailedMessage) Using default marker icon.") | ||
| } | ||
| } | ||
| } else { | ||
| if let color = marker.color { | ||
| newMarker.icon = GMSMarker.markerImage(with: color) | ||
| } | ||
| } | ||
| return newMarker | ||
| } | ||
| } | ||
| private func getResizedIcon(_ iconImage: UIImage, _ marker: Marker) -> UIImage? { | ||
| if let iconSize = marker.iconSize { | ||
| return iconImage.resizeImageTo(size: iconSize) | ||
| } else { | ||
| return iconImage | ||
| } | ||
| } | ||
| extension WKWebView { | ||
| static var disabledTargets: [UIView] = [] | ||
| override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { | ||
| var hitView = super.hitTest(point, with: event) | ||
| if let tempHitView = hitView, WKWebView.disabledTargets.contains(tempHitView) { | ||
| return nil | ||
| } | ||
| if let typeClass = NSClassFromString("WKChildScrollView"), let tempHitView = hitView, tempHitView.isKind(of: typeClass) { | ||
| for item in tempHitView.subviews.reversed() { | ||
| let convertPoint = item.convert(point, from: self) | ||
| if let hitTestView = item.hitTest(convertPoint, with: event) { | ||
| hitView = hitTestView | ||
| break | ||
| } | ||
| } | ||
| } | ||
| return hitView | ||
| } | ||
| } | ||
| extension UIView { | ||
| private static var allSubviews: [UIView] = [] | ||
| private func viewArray(root: UIView) -> [UIView] { | ||
| var index = root.tag | ||
| for view in root.subviews { | ||
| if view.tag == Map.MAP_TAG { | ||
| // view already in use as in map | ||
| continue | ||
| } | ||
| // tag the index depth of the uiview | ||
| view.tag = index | ||
| if view.isKind(of: UIView.self) { | ||
| UIView.allSubviews.append(view) | ||
| } | ||
| _ = viewArray(root: view) | ||
| index += 1 | ||
| } | ||
| return UIView.allSubviews | ||
| } | ||
| fileprivate func getAllSubViews() -> [UIView] { | ||
| UIView.allSubviews = [] | ||
| return viewArray(root: self).reversed() | ||
| } | ||
| fileprivate func removeAllSubview() { | ||
| subviews.forEach { | ||
| $0.removeFromSuperview() | ||
| } | ||
| } | ||
| } | ||
| extension UIImage { | ||
| func resizeImageTo(size: CGSize) -> UIImage? { | ||
| UIGraphicsBeginImageContextWithOptions(size, false, 0.0) | ||
| self.draw(in: CGRect(origin: CGPoint.zero, size: size)) | ||
| let resizedImage = UIGraphicsGetImageFromCurrentImageContext()! | ||
| UIGraphicsEndImageContext() | ||
| return resizedImage | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct Marker { | ||
| let coordinate: LatLng | ||
| let opacity: Float? | ||
| let title: String? | ||
| let snippet: String? | ||
| let isFlat: Bool? | ||
| let iconUrl: String? | ||
| let iconSize: CGSize? | ||
| let iconAnchor: CGPoint? | ||
| let draggable: Bool? | ||
| let color: UIColor? | ||
| let zIndex: Int32 | ||
| init(fromJSObject: JSObject) throws { | ||
| guard let latLngObj = fromJSObject["coordinate"] as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("Marker object is missing the required 'coordinate' property") | ||
| } | ||
| guard let lat = latLngObj["lat"] as? Double, let lng = latLngObj["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| var iconSize: CGSize? | ||
| if let sizeObj = fromJSObject["iconSize"] as? JSObject { | ||
| if let width = sizeObj["width"] as? Double, let height = sizeObj["height"] as? Double { | ||
| iconSize = CGSize(width: width, height: height) | ||
| } | ||
| } | ||
| var iconAnchor: CGPoint? | ||
| if let anchorObject = fromJSObject["iconAnchor"] as? JSObject { | ||
| if let x = anchorObject["x"] as? Double, let y = anchorObject["y"] as? Double { | ||
| if let size = iconSize { | ||
| let u = x / size.width | ||
| let v = y / size.height | ||
| iconAnchor = CGPoint(x: u, y: v) | ||
| } | ||
| } | ||
| } | ||
| var tintColor: UIColor? | ||
| if let rgbObject = fromJSObject["tintColor"] as? JSObject { | ||
| if let r = rgbObject["r"] as? Double, let g = rgbObject["g"] as? Double, let b = rgbObject["b"] as? Double, let a = rgbObject["a"] as? Double { | ||
| let uiColorR = CGFloat(r / 255).clamp(min: 0, max: 255) | ||
| let uiColorG = CGFloat(g / 255).clamp(min: 0, max: 255) | ||
| let uiColorB = CGFloat(b / 255).clamp(min: 0, max: 255) | ||
| tintColor = UIColor(red: uiColorR, green: uiColorG, blue: uiColorB, alpha: CGFloat(a)) | ||
| } | ||
| } | ||
| self.coordinate = LatLng(lat: lat, lng: lng) | ||
| self.opacity = fromJSObject["opacity"] as? Float | ||
| self.title = fromJSObject["title"] as? String | ||
| self.snippet = fromJSObject["snippet"] as? String | ||
| self.isFlat = fromJSObject["isFlat"] as? Bool | ||
| self.iconUrl = fromJSObject["iconUrl"] as? String | ||
| self.draggable = fromJSObject["draggable"] as? Bool | ||
| self.iconSize = iconSize | ||
| self.iconAnchor = iconAnchor | ||
| self.color = tintColor | ||
| self.zIndex = Int32((fromJSObject["zIndex"] as? Int) ?? 0) | ||
| } | ||
| } | ||
| extension CGFloat { | ||
| func clamp(min: CGFloat, max: CGFloat) -> CGFloat { | ||
| if self < min { | ||
| return min | ||
| } | ||
| if self > max { | ||
| return max | ||
| } | ||
| return self | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct Polygon { | ||
| let shapes: [[LatLng]] | ||
| let strokeWidth: CGFloat | ||
| let strokeColor: UIColor | ||
| let fillColor: UIColor | ||
| let tappable: Bool? | ||
| let geodesic: Bool? | ||
| let title: String? | ||
| let zIndex: Int32 | ||
| let tag: String? | ||
| init(fromJSObject: JSObject) throws { | ||
| var strokeColor = UIColor.blue | ||
| var strokeWidth: CGFloat = 1.0 | ||
| var fillColor = UIColor.blue | ||
| var processedShapes: [[LatLng]] = [] | ||
| if let width = fromJSObject["strokeWeight"] as? Float { | ||
| strokeWidth = CGFloat(width) | ||
| } | ||
| let strokeOpacity = fromJSObject["strokeOpacity"] as? Double | ||
| if let hexColor = fromJSObject["strokeColor"] as? String { | ||
| strokeColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| strokeColor = strokeColor.withAlphaComponent(strokeOpacity ?? 1.0) | ||
| let fillOpacity = fromJSObject["fillOpacity"] as? Double | ||
| if let hexColor = fromJSObject["fillColor"] as? String { | ||
| fillColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| fillColor = fillColor.withAlphaComponent(fillOpacity ?? 1.0) | ||
| guard let shapeJSArray = fromJSObject["paths"] as? JSArray else { | ||
| throw GoogleMapErrors.invalidArguments("Polygon object is missing the required 'paths' property") | ||
| } | ||
| if let obj = shapeJSArray.first, obj as? JSArray != nil { | ||
| try shapeJSArray.forEach({ obj in | ||
| if let shapeArr = obj as? JSArray { | ||
| try processedShapes.append(Polygon.processShape(shapeArr)) | ||
| } | ||
| }) | ||
| } else { | ||
| // is a single shape | ||
| try processedShapes.append(Polygon.processShape(shapeJSArray)) | ||
| } | ||
| self.shapes = processedShapes | ||
| self.fillColor = fillColor | ||
| self.strokeColor = strokeColor | ||
| self.strokeWidth = strokeWidth | ||
| self.tag = fromJSObject["tag"] as? String | ||
| self.tappable = fromJSObject["clickable"] as? Bool | ||
| self.title = fromJSObject["title"] as? String | ||
| self.geodesic = fromJSObject["geodesic"] as? Bool | ||
| self.zIndex = Int32((fromJSObject["zIndex"] as? Int) ?? 0) | ||
| } | ||
| private static func processShape(_ shapeArr: JSArray) throws -> [LatLng] { | ||
| var shape: [LatLng] = [] | ||
| try shapeArr.forEach { obj in | ||
| guard let jsCoord = obj as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| guard let lat = jsCoord["lat"] as? Double, let lng = jsCoord["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| shape.append(LatLng(lat: lat, lng: lng)) | ||
| } | ||
| return shape | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct StyleSpan { | ||
| let color: UIColor | ||
| let segments: Double? | ||
| } | ||
| public struct Polyline { | ||
| let path: [LatLng] | ||
| let strokeWidth: CGFloat | ||
| let strokeColor: UIColor | ||
| let title: String? | ||
| let tappable: Bool? | ||
| let geodesic: Bool? | ||
| let zIndex: Int32 | ||
| let tag: String? | ||
| let styleSpans: [StyleSpan] | ||
| init(fromJSObject: JSObject) throws { | ||
| var strokeColor = UIColor.blue | ||
| var strokeWidth: CGFloat = 1.0 | ||
| var path: [LatLng] = [] | ||
| var styleSpans: [StyleSpan] = [] | ||
| if let width = fromJSObject["strokeWeight"] as? Float { | ||
| strokeWidth = CGFloat(width) | ||
| } | ||
| let strokeOpacity = fromJSObject["strokeOpacity"] as? Double | ||
| if let hexColor = fromJSObject["strokeColor"] as? String { | ||
| strokeColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| strokeColor = strokeColor.withAlphaComponent(strokeOpacity ?? 1.0) | ||
| guard let pathJSArray = fromJSObject["path"] as? JSArray else { | ||
| throw GoogleMapErrors.invalidArguments("Polyline object is missing the required 'path' property") | ||
| } | ||
| try pathJSArray.forEach { obj in | ||
| guard let jsCoord = obj as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| guard let lat = jsCoord["lat"] as? Double, let lng = jsCoord["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| path.append(LatLng(lat: lat, lng: lng)) | ||
| } | ||
| if let styleSpanJSArray = fromJSObject["styleSpans"] as? JSArray { | ||
| styleSpanJSArray.forEach({ obj in | ||
| if let styleSpanObj = obj as? JSObject, | ||
| let hexColor = styleSpanObj["color"] as? String, | ||
| let color = UIColor(hex: hexColor) { | ||
| let segments = styleSpanObj["segments"] as? Double | ||
| styleSpans.append(StyleSpan(color: color, segments: segments)) | ||
| } | ||
| }) | ||
| } | ||
| self.strokeColor = strokeColor | ||
| self.strokeWidth = strokeWidth | ||
| self.tag = fromJSObject["tag"] as? String | ||
| self.title = fromJSObject["title"] as? String | ||
| self.tappable = fromJSObject["clickable"] as? Bool | ||
| self.geodesic = fromJSObject["geodesic"] as? Bool | ||
| self.zIndex = Int32((fromJSObject["zIndex"] as? Int) ?? 0) | ||
| self.path = path | ||
| self.styleSpans = styleSpans | ||
| } | ||
| } |
| import Capacitor | ||
| public struct TileOverlay: Codable { | ||
| let url: String | ||
| let opacity: Float? | ||
| let visible: Bool? | ||
| let zIndex: Int32 | ||
| init(fromJSObject: JSObject) throws { | ||
| guard let url = fromJSObject["url"] as? String else { | ||
| throw GoogleMapErrors.invalidArguments("TileOverlay object is missing the required 'url' property") | ||
| } | ||
| self.url = url | ||
| self.opacity = fromJSObject["opacity"] as? Float | ||
| self.visible = fromJSObject["isFlat"] as? Bool | ||
| self.zIndex = Int32((fromJSObject["zIndex"] as? Int) ?? 0) | ||
| } | ||
| } |
| import XCTest | ||
| @testable import Plugin | ||
| class CapacitorGoogleMapsTests: XCTestCase { | ||
| override func setUp() { | ||
| super.setUp() | ||
| // Put setup code here. This method is called before the invocation of each test method in the class. | ||
| } | ||
| override func tearDown() { | ||
| // Put teardown code here. This method is called after the invocation of each test method in the class. | ||
| super.tearDown() | ||
| } | ||
| func testEcho() { | ||
| // This is an example of a functional test case for a plugin. | ||
| // Use XCTAssert and related functions to verify your tests produce the correct results. | ||
| let implementation = CapacitorGoogleMaps() | ||
| let value = "Hello, World!" | ||
| let result = implementation.echo(value) | ||
| XCTAssertEqual(value, result) | ||
| } | ||
| } |
| // swift-tools-version: 5.9 | ||
| import PackageDescription | ||
| let package = Package( | ||
| name: "CapacitorGoogleMaps", | ||
| platforms: [.iOS(.v14)], | ||
| products: [ | ||
| .library( | ||
| name: "CapacitorGoogleMaps", | ||
| targets: ["CapacitorGoogleMapsPlugin"]) | ||
| ], | ||
| dependencies: [ | ||
| .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0"), | ||
| .package(url: "https://github.com/googlemaps/ios-maps-sdk.git", .upToNextMajor(from:"8.4.0")), | ||
| .package(url: "https://github.com/googlemaps/google-maps-ios-utils.git", .upToNextMajor(from:"5.0.0")) | ||
| ], | ||
| targets: [ | ||
| .target( | ||
| name: "CapacitorGoogleMapsPlugin", | ||
| dependencies: [ | ||
| .product(name: "Capacitor", package: "capacitor-swift-pm"), | ||
| .product(name: "Cordova", package: "capacitor-swift-pm"), | ||
| .product(name: "GoogleMaps", package: "ios-maps-sdk"), | ||
| .product(name: "GoogleMapsUtils", package: "google-maps-ios-utils") | ||
| ], | ||
| path: "ios/Sources/CapacitorGoogleMapsPlugin"), | ||
| .testTarget( | ||
| name: "CapacitorGoogleMapsPluginTests", | ||
| dependencies: ["CapacitorGoogleMapsPlugin"], | ||
| path: "ios/Tests/CapacitorGoogleMapsPluginTests") | ||
| ] | ||
| ) |
+7
-6
| { | ||
| "name": "@capacitor/google-maps", | ||
| "version": "7.1.1", | ||
| "version": "7.2.0-spm-dev.1", | ||
| "description": "Google maps on Capacitor", | ||
@@ -20,4 +20,6 @@ "main": "dist/plugin.cjs.js", | ||
| "dist/", | ||
| "ios/Plugin/", | ||
| "CapacitorGoogleMaps.podspec" | ||
| "CapacitorGoogleMaps.podspec", | ||
| "ios/Sources", | ||
| "ios/Tests", | ||
| "Package.swift" | ||
| ], | ||
@@ -41,3 +43,3 @@ "author": "Ionic <hi@ionicframework.com>", | ||
| "verify": "pnpm run verify:ios && pnpm run verify:android && pnpm run verify:web", | ||
| "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin -sdk iphonesimulator && cd ..", | ||
| "verify:ios": "xcodebuild build -scheme CapacitorGoogleMaps -destination generic/platform=iOS", | ||
| "verify:android": "cd android && ./gradlew clean build test && cd ..", | ||
@@ -97,4 +99,3 @@ "verify:web": "pnpm run build", | ||
| "@types/google.maps": "~3.58.1" | ||
| }, | ||
| "gitHead": "f08a955e6a421a5132f0b9d4c98401c6ebe001ec" | ||
| } | ||
| } |
| #import <UIKit/UIKit.h> | ||
| //! Project version number for Plugin. | ||
| FOUNDATION_EXPORT double PluginVersionNumber; | ||
| //! Project version string for Plugin. | ||
| FOUNDATION_EXPORT const unsigned char PluginVersionString[]; | ||
| // In this header, you should import all the public headers of your framework using statements like #import <Plugin/PublicHeader.h> | ||
| #import <Foundation/Foundation.h> | ||
| #import <Capacitor/Capacitor.h> | ||
| // Define the plugin using the CAP_PLUGIN Macro, and | ||
| // each method the plugin supports using the CAP_PLUGIN_METHOD macro. | ||
| CAP_PLUGIN(CapacitorGoogleMapsPlugin, "CapacitorGoogleMaps", | ||
| CAP_PLUGIN_METHOD(create, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(enableTouch, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(disableTouch, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(addTileOverlay, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(removeTileOverlay, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(addMarker, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(addMarkers, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(addPolygons, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(addPolylines, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(addCircles, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(removeMarker, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(removeMarkers, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(removeCircles, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(removePolygons, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(removePolylines, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(enableClustering, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(disableClustering, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(destroy, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(setCamera, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(getMapType, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(setMapType, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(enableIndoorMaps, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(enableTrafficLayer, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(enableAccessibilityElements, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(enableCurrentLocation, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(setPadding, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(onScroll, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(onResize, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(onDisplay, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(getMapBounds, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(fitBounds, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(mapBoundsContains, CAPPluginReturnPromise); | ||
| CAP_PLUGIN_METHOD(mapBoundsExtend, CAPPluginReturnPromise); | ||
| ) |
| // swiftlint:disable file_length | ||
| import Foundation | ||
| import Capacitor | ||
| import GoogleMaps | ||
| import GoogleMapsUtils | ||
| extension GMSMapViewType { | ||
| static func fromString(mapType: String) -> GMSMapViewType { | ||
| switch mapType { | ||
| case "Normal": | ||
| return .normal | ||
| case "Hybrid": | ||
| return .hybrid | ||
| case "Satellite": | ||
| return .satellite | ||
| case "Terrain": | ||
| return .terrain | ||
| case "None": | ||
| return .none | ||
| default: | ||
| print("CapacitorGoogleMaps Warning: unknown mapView type '\(mapType)'. Defaulting to normal.") | ||
| return .normal | ||
| } | ||
| } | ||
| static func toString(mapType: GMSMapViewType) -> String { | ||
| switch mapType { | ||
| case .normal: | ||
| return "Normal" | ||
| case .hybrid: | ||
| return "Hybrid" | ||
| case .satellite: | ||
| return "Satellite" | ||
| case .terrain: | ||
| return "Terrain" | ||
| case .none: | ||
| return "None" | ||
| default: | ||
| return "Normal" | ||
| } | ||
| } | ||
| } | ||
| extension CGRect { | ||
| static func fromJSObject(_ jsObject: JSObject) throws -> CGRect { | ||
| guard let width = jsObject["width"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("bounds object is missing the required 'width' property") | ||
| } | ||
| guard let height = jsObject["height"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("bounds object is missing the required 'height' property") | ||
| } | ||
| guard let x = jsObject["x"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("bounds object is missing the required 'x' property") | ||
| } | ||
| guard let y = jsObject["y"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("bounds object is missing the required 'y' property") | ||
| } | ||
| return CGRect(x: x, y: y, width: width, height: height) | ||
| } | ||
| } | ||
| // swiftlint:disable type_body_length | ||
| @objc(CapacitorGoogleMapsPlugin) | ||
| public class CapacitorGoogleMapsPlugin: CAPPlugin, GMSMapViewDelegate { | ||
| private var maps = [String: Map]() | ||
| private var isInitialized = false | ||
| private var locationManager = CLLocationManager() | ||
| func checkLocationPermission() -> String { | ||
| let locationState: String | ||
| switch self.locationManager.authorizationStatus { | ||
| case .notDetermined: | ||
| locationState = "prompt" | ||
| case .restricted, .denied: | ||
| locationState = "denied" | ||
| case .authorizedAlways, .authorizedWhenInUse: | ||
| locationState = "granted" | ||
| @unknown default: | ||
| locationState = "prompt" | ||
| } | ||
| return locationState | ||
| } | ||
| @objc func create(_ call: CAPPluginCall) { | ||
| do { | ||
| if !isInitialized { | ||
| guard let apiKey = call.getString("apiKey") else { | ||
| throw GoogleMapErrors.invalidAPIKey | ||
| } | ||
| GMSServices.provideAPIKey(apiKey) | ||
| isInitialized = true | ||
| } | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let configObj = call.getObject("config") else { | ||
| throw GoogleMapErrors.invalidArguments("config object is missing") | ||
| } | ||
| let forceCreate = call.getBool("forceCreate", false) | ||
| let config = try GoogleMapConfig(fromJSObject: configObj) | ||
| if self.maps[id] != nil { | ||
| if !forceCreate { | ||
| call.resolve() | ||
| return | ||
| } | ||
| let removedMap = self.maps.removeValue(forKey: id) | ||
| removedMap?.destroy() | ||
| } | ||
| DispatchQueue.main.sync { | ||
| let newMap = Map(id: id, config: config, delegate: self) | ||
| self.maps[id] = newMap | ||
| } | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func destroy(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let removedMap = self.maps.removeValue(forKey: id) else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| removedMap.destroy() | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableTouch(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| map.enableTouch() | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func disableTouch(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| map.disableTouch() | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addTileOverlay(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let tileOverlayObj = call.getObject("tileOverlay") else { | ||
| throw GoogleMapErrors.invalidArguments("tileOverlay object is missing") | ||
| } | ||
| let tileOverlay = try TileOverlay(fromJSObject: tileOverlayObj) | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| let tileOverlayId = try map.addTileOverlay(tileOverlay: tileOverlay) | ||
| call.resolve(["id": String(tileOverlayId)]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removeTileOverlay(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let tileOverlayIdString = call.getString("tileOverlayId") else { | ||
| throw GoogleMapErrors.invalidArguments("tileOverlayId is invalid or missing") | ||
| } | ||
| guard let tileOverlayId = Int(tileOverlayIdString) else { | ||
| throw GoogleMapErrors.invalidArguments("tileOverlayId is invalid or missing") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removeTileOverlay(id: tileOverlayId) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addMarker(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let markerObj = call.getObject("marker") else { | ||
| throw GoogleMapErrors.invalidArguments("marker object is missing") | ||
| } | ||
| let marker = try Marker(fromJSObject: markerObj) | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| let markerId = try map.addMarker(marker: marker) | ||
| call.resolve(["id": String(markerId)]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addMarkers(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let markerObjs = call.getArray("markers") as? [JSObject] else { | ||
| throw GoogleMapErrors.invalidArguments("markers array is missing") | ||
| } | ||
| if markerObjs.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("markers requires at least one marker") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| var markers: [Marker] = [] | ||
| try markerObjs.forEach { marker in | ||
| let marker = try Marker(fromJSObject: marker) | ||
| markers.append(marker) | ||
| } | ||
| let ids = try map.addMarkers(markers: markers) | ||
| call.resolve(["ids": ids.map({ id in | ||
| return String(id) | ||
| })]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removeMarkers(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let markerIdStrings = call.getArray("markerIds") as? [String] else { | ||
| throw GoogleMapErrors.invalidArguments("markerIds are invalid or missing") | ||
| } | ||
| if markerIdStrings.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("markerIds requires at least one marker id") | ||
| } | ||
| let ids: [Int] = try markerIdStrings.map { idString in | ||
| guard let markerId = Int(idString) else { | ||
| throw GoogleMapErrors.invalidArguments("markerIds are invalid or missing") | ||
| } | ||
| return markerId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removeMarkers(ids: ids) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removeMarker(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let markerIdString = call.getString("markerId") else { | ||
| throw GoogleMapErrors.invalidArguments("markerId is invalid or missing") | ||
| } | ||
| guard let markerId = Int(markerIdString) else { | ||
| throw GoogleMapErrors.invalidArguments("markerId is invalid or missing") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removeMarker(id: markerId) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addPolygons(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let shapeObjs = call.getArray("polygons") as? [JSObject] else { | ||
| throw GoogleMapErrors.invalidArguments("polygons array is missing") | ||
| } | ||
| if shapeObjs.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("polygons requires at least one shape") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| var shapes: [Polygon] = [] | ||
| try shapeObjs.forEach { shapeObj in | ||
| let polygon = try Polygon(fromJSObject: shapeObj) | ||
| shapes.append(polygon) | ||
| } | ||
| let ids = try map.addPolygons(polygons: shapes) | ||
| call.resolve(["ids": ids.map({ id in | ||
| return String(id) | ||
| })]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addPolylines(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let lineObjs = call.getArray("polylines") as? [JSObject] else { | ||
| throw GoogleMapErrors.invalidArguments("polylines array is missing") | ||
| } | ||
| if lineObjs.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("polylines requires at least one line") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| var lines: [Polyline] = [] | ||
| try lineObjs.forEach { lineObj in | ||
| let line = try Polyline(fromJSObject: lineObj) | ||
| lines.append(line) | ||
| } | ||
| let ids = try map.addPolylines(lines: lines) | ||
| call.resolve(["ids": ids.map({ id in | ||
| return String(id) | ||
| })]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removePolygons(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let polygonIdsStrings = call.getArray("polygonIds") as? [String] else { | ||
| throw GoogleMapErrors.invalidArguments("polygonIds are invalid or missing") | ||
| } | ||
| if polygonIdsStrings.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("polygonIds requires at least one polygon id") | ||
| } | ||
| let ids: [Int] = try polygonIdsStrings.map { idString in | ||
| guard let polygonId = Int(idString) else { | ||
| throw GoogleMapErrors.invalidArguments("polygonIds are invalid or missing") | ||
| } | ||
| return polygonId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removePolygons(ids: ids) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func addCircles(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let circleObjs = call.getArray("circles") as? [JSObject] else { | ||
| throw GoogleMapErrors.invalidArguments("circles array is missing") | ||
| } | ||
| if circleObjs.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("circles requires at least one circle") | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| var circles: [Circle] = [] | ||
| try circleObjs.forEach { circleObj in | ||
| let circle = try Circle(from: circleObj) | ||
| circles.append(circle) | ||
| } | ||
| let ids = try map.addCircles(circles: circles) | ||
| call.resolve(["ids": ids.map({ id in | ||
| return String(id) | ||
| })]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removeCircles(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let circleIdsStrings = call.getArray("circleIds") as? [String] else { | ||
| throw GoogleMapErrors.invalidArguments("circleIds are invalid or missing") | ||
| } | ||
| if circleIdsStrings.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("circleIds requires at least one cicle id") | ||
| } | ||
| let ids: [Int] = try circleIdsStrings.map { idString in | ||
| guard let circleId = Int(idString) else { | ||
| throw GoogleMapErrors.invalidArguments("circleIds are invalid or missing") | ||
| } | ||
| return circleId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removeCircles(ids: ids) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func removePolylines(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let polylineIdsStrings = call.getArray("polylineIds") as? [String] else { | ||
| throw GoogleMapErrors.invalidArguments("polylineIds are invalid or missing") | ||
| } | ||
| if polylineIdsStrings.isEmpty { | ||
| throw GoogleMapErrors.invalidArguments("polylineIds requires at least one polyline id") | ||
| } | ||
| let ids: [Int] = try polylineIdsStrings.map { idString in | ||
| guard let polylineId = Int(idString) else { | ||
| throw GoogleMapErrors.invalidArguments("polylineIds are invalid or missing") | ||
| } | ||
| return polylineId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try map.removePolylines(ids: ids) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func setCamera(_ 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 configObj = call.getObject("config") else { | ||
| throw GoogleMapErrors.invalidArguments("config object is missing") | ||
| } | ||
| let config = try GoogleMapCameraConfig(fromJSObject: configObj) | ||
| try map.setCamera(config: config) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func getMapType(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| let mapType = GMSMapViewType.toString(mapType: map.getMapType()) | ||
| call.resolve([ | ||
| "type": mapType | ||
| ]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func setMapType(_ 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 mapTypeString = call.getString("mapType") else { | ||
| throw GoogleMapErrors.invalidArguments("mapType is missing") | ||
| } | ||
| let mapType = GMSMapViewType.fromString(mapType: mapTypeString) | ||
| try map.setMapType(mapType: mapType) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableIndoorMaps(_ 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 enabled = call.getBool("enabled") else { | ||
| throw GoogleMapErrors.invalidArguments("enabled is missing") | ||
| } | ||
| try map.enableIndoorMaps(enabled: enabled) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableTrafficLayer(_ 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 enabled = call.getBool("enabled") else { | ||
| throw GoogleMapErrors.invalidArguments("enabled is missing") | ||
| } | ||
| try map.enableTrafficLayer(enabled: enabled) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableAccessibilityElements(_ 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 enabled = call.getBool("enabled") else { | ||
| throw GoogleMapErrors.invalidArguments("enabled is missing") | ||
| } | ||
| try map.enableAccessibilityElements(enabled: enabled) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func setPadding(_ 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 configObj = call.getObject("padding") else { | ||
| throw GoogleMapErrors.invalidArguments("padding is missing") | ||
| } | ||
| let padding = try GoogleMapPadding.init(fromJSObject: configObj) | ||
| try map.setPadding(padding: padding) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableCurrentLocation(_ 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 enabled = call.getBool("enabled") else { | ||
| throw GoogleMapErrors.invalidArguments("enabled is missing") | ||
| } | ||
| let locationStatus = checkLocationPermission() | ||
| if enabled && !(locationStatus == "granted" || locationStatus == "prompt") { | ||
| throw GoogleMapErrors.permissionsDeniedLocation | ||
| } | ||
| try map.enableCurrentLocation(enabled: enabled) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func enableClustering(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| let minClusterSize = call.getInt("minClusterSize") | ||
| map.enableClustering(minClusterSize) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func disableClustering(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| map.disableClustering() | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func onScroll(_ call: CAPPluginCall) { | ||
| call.unavailable("not supported on iOS") | ||
| } | ||
| @objc func onResize(_ 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 mapBoundsObj = call.getObject("mapBounds") else { | ||
| throw GoogleMapErrors.invalidArguments("map bounds not set") | ||
| } | ||
| let mapBounds = try CGRect.fromJSObject(mapBoundsObj) | ||
| map.updateRender(mapBounds: mapBounds) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func onDisplay(_ 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 mapBoundsObj = call.getObject("mapBounds") else { | ||
| throw GoogleMapErrors.invalidArguments("map bounds not set") | ||
| } | ||
| let mapBounds = try CGRect.fromJSObject(mapBoundsObj) | ||
| map.rebindTargetContainer(mapBounds: mapBounds) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func getMapBounds(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let id = call.getString("id") else { | ||
| throw GoogleMapErrors.invalidMapId | ||
| } | ||
| guard let map = self.maps[id] else { | ||
| throw GoogleMapErrors.mapNotFound | ||
| } | ||
| try DispatchQueue.main.sync { | ||
| guard let bounds = map.getMapLatLngBounds() else { | ||
| throw GoogleMapErrors.unhandledError("Google Map Bounds could not be found.") | ||
| } | ||
| call.resolve( | ||
| formatMapBoundsForResponse( | ||
| bounds: bounds, | ||
| cameraPosition: map.mapViewController.GMapView.camera | ||
| ) | ||
| ) | ||
| } | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func mapBoundsContains(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let boundsObject = call.getObject("bounds") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid bounds provided") | ||
| } | ||
| guard let pointObject = call.getObject("point") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid point provided") | ||
| } | ||
| let bounds = try getGMSCoordinateBounds(boundsObject) | ||
| let point = try getCLLocationCoordinate(pointObject) | ||
| call.resolve([ | ||
| "contains": bounds.contains(point) | ||
| ]) | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func fitBounds(_ 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 boundsObject = call.getObject("bounds") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid bounds provided") | ||
| } | ||
| let bounds = try getGMSCoordinateBounds(boundsObject) | ||
| let padding = CGFloat(call.getInt("padding", 0)) | ||
| map.fitBounds(bounds: bounds, padding: padding) | ||
| call.resolve() | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| @objc func mapBoundsExtend(_ call: CAPPluginCall) { | ||
| do { | ||
| guard let boundsObject = call.getObject("bounds") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid bounds provided") | ||
| } | ||
| guard let pointObject = call.getObject("point") else { | ||
| throw GoogleMapErrors.invalidArguments("Invalid point provided") | ||
| } | ||
| let bounds = try getGMSCoordinateBounds(boundsObject) | ||
| let point = try getCLLocationCoordinate(pointObject) | ||
| DispatchQueue.main.sync { | ||
| let newBounds = bounds.includingCoordinate(point) | ||
| call.resolve([ | ||
| "bounds": formatMapBoundsForResponse(newBounds) | ||
| ]) | ||
| } | ||
| } catch { | ||
| handleError(call, error: error) | ||
| } | ||
| } | ||
| private func getGMSCoordinateBounds(_ bounds: JSObject) throws -> GMSCoordinateBounds { | ||
| guard let southwest = bounds["southwest"] as? JSObject else { | ||
| throw GoogleMapErrors.unhandledError("Bounds southwest property not formatted properly.") | ||
| } | ||
| guard let northeast = bounds["northeast"] as? JSObject else { | ||
| throw GoogleMapErrors.unhandledError("Bounds northeast property not formatted properly.") | ||
| } | ||
| return GMSCoordinateBounds( | ||
| coordinate: try getCLLocationCoordinate(southwest), | ||
| coordinate: try getCLLocationCoordinate(northeast) | ||
| ) | ||
| } | ||
| private func getCLLocationCoordinate(_ point: JSObject) throws -> CLLocationCoordinate2D { | ||
| guard let lat = point["lat"] as? Double else { | ||
| throw GoogleMapErrors.unhandledError("Point lat property not formatted properly.") | ||
| } | ||
| guard let lng = point["lng"] as? Double else { | ||
| throw GoogleMapErrors.unhandledError("Point lng property not formatted properly.") | ||
| } | ||
| return CLLocationCoordinate2D(latitude: lat, longitude: lng) | ||
| } | ||
| private func formatMapBoundsForResponse(bounds: GMSCoordinateBounds?, cameraPosition: GMSCameraPosition) -> PluginCallResultData { | ||
| return [ | ||
| "southwest": [ | ||
| "lat": bounds?.southWest.latitude, | ||
| "lng": bounds?.southWest.longitude | ||
| ], | ||
| "center": [ | ||
| "lat": cameraPosition.target.latitude, | ||
| "lng": cameraPosition.target.longitude | ||
| ], | ||
| "northeast": [ | ||
| "lat": bounds?.northEast.latitude, | ||
| "lng": bounds?.northEast.longitude | ||
| ] | ||
| ] | ||
| } | ||
| private func formatMapBoundsForResponse(_ bounds: GMSCoordinateBounds) -> PluginCallResultData { | ||
| let centerLatitude = (bounds.southWest.latitude + bounds.northEast.latitude) / 2.0 | ||
| let centerLongitude = (bounds.southWest.longitude + bounds.northEast.longitude) / 2.0 | ||
| return [ | ||
| "southwest": [ | ||
| "lat": bounds.southWest.latitude, | ||
| "lng": bounds.southWest.longitude | ||
| ], | ||
| "center": [ | ||
| "lat": centerLatitude, | ||
| "lng": centerLongitude | ||
| ], | ||
| "northeast": [ | ||
| "lat": bounds.northEast.latitude, | ||
| "lng": bounds.northEast.longitude | ||
| ] | ||
| ] | ||
| } | ||
| private func handleError(_ call: CAPPluginCall, error: Error) { | ||
| let errObject = getErrorObject(error) | ||
| call.reject(errObject.message, "\(errObject.code)", error, [:]) | ||
| } | ||
| private func findMapIdByMapView(_ mapView: GMSMapView) -> String { | ||
| for (mapId, map) in self.maps { | ||
| if map.mapViewController.GMapView === mapView { | ||
| return mapId | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
| // --- EVENT LISTENERS --- | ||
| // onCameraIdle | ||
| public func mapView(_ mapView: GMSMapView, idleAt cameraPosition: GMSCameraPosition) { | ||
| let mapId = self.findMapIdByMapView(mapView) | ||
| let map = self.maps[mapId] | ||
| let bounds = map?.getMapLatLngBounds() | ||
| let data: PluginCallResultData = [ | ||
| "mapId": mapId, | ||
| "bounds": formatMapBoundsForResponse( | ||
| bounds: bounds, | ||
| cameraPosition: cameraPosition | ||
| ), | ||
| "bearing": cameraPosition.bearing, | ||
| "latitude": cameraPosition.target.latitude, | ||
| "longitude": cameraPosition.target.longitude, | ||
| "tilt": cameraPosition.viewingAngle, | ||
| "zoom": cameraPosition.zoom | ||
| ] | ||
| self.notifyListeners("onBoundsChanged", data: data) | ||
| self.notifyListeners("onCameraIdle", data: data) | ||
| } | ||
| // onCameraMoveStarted | ||
| public func mapView(_ mapView: GMSMapView, willMove gesture: Bool) { | ||
| self.notifyListeners("onCameraMoveStarted", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "isGesture": gesture | ||
| ]) | ||
| } | ||
| // onMapClick | ||
| public func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) { | ||
| self.notifyListeners("onMapClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "latitude": coordinate.latitude, | ||
| "longitude": coordinate.longitude | ||
| ]) | ||
| } | ||
| // onPolygonClick, onPolylineClick, onCircleClick | ||
| public func mapView(_ mapView: GMSMapView, didTap overlay: GMSOverlay) { | ||
| if let polygon = overlay as? GMSPolygon { | ||
| self.notifyListeners("onPolygonClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "polygonId": String(overlay.hash.hashValue), | ||
| "tag": polygon.userData as? String | ||
| ]) | ||
| } | ||
| if let circle = overlay as? GMSCircle { | ||
| self.notifyListeners("onCircleClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "circleId": String(overlay.hash.hashValue), | ||
| "tag": circle.userData as? String, | ||
| "latitude": circle.position.latitude, | ||
| "longitude": circle.position.longitude, | ||
| "radius": circle.radius | ||
| ]) | ||
| } | ||
| if let polyline = overlay as? GMSPolyline { | ||
| self.notifyListeners("onPolylineClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "polylineId": String(overlay.hash.hashValue), | ||
| "tag": polyline.userData as? String | ||
| ]) | ||
| } | ||
| } | ||
| // onClusterClick, onMarkerClick | ||
| public func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { | ||
| if let cluster = marker.userData as? GMUCluster { | ||
| var items: [[String: Any?]] = [] | ||
| for item in cluster.items { | ||
| items.append([ | ||
| "markerId": String(item.hash.hashValue), | ||
| "latitude": item.position.latitude, | ||
| "longitude": item.position.longitude, | ||
| "title": item.title ?? "", | ||
| "snippet": item.snippet ?? "" | ||
| ]) | ||
| } | ||
| self.notifyListeners("onClusterClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "latitude": cluster.position.latitude, | ||
| "longitude": cluster.position.longitude, | ||
| "size": cluster.count, | ||
| "items": items | ||
| ]) | ||
| } else { | ||
| self.notifyListeners("onMarkerClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| return false | ||
| } | ||
| // onMarkerDragStart | ||
| public func mapView(_ mapView: GMSMapView, didBeginDragging marker: GMSMarker) { | ||
| self.notifyListeners("onMarkerDragStart", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| // onMarkerDrag | ||
| public func mapView(_ mapView: GMSMapView, didDrag marker: GMSMarker) { | ||
| self.notifyListeners("onMarkerDrag", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| // onMarkerDragEnd | ||
| public func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) { | ||
| self.notifyListeners("onMarkerDragEnd", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| // onClusterInfoWindowClick, onInfoWindowClick | ||
| public func mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker) { | ||
| if let cluster = marker.userData as? GMUCluster { | ||
| var items: [[String: Any?]] = [] | ||
| for item in cluster.items { | ||
| items.append([ | ||
| "markerId": String(item.hash.hashValue), | ||
| "latitude": item.position.latitude, | ||
| "longitude": item.position.longitude, | ||
| "title": item.title ?? "", | ||
| "snippet": item.snippet ?? "" | ||
| ]) | ||
| } | ||
| self.notifyListeners("onClusterInfoWindowClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "latitude": cluster.position.latitude, | ||
| "longitude": cluster.position.longitude, | ||
| "size": cluster.count, | ||
| "items": items | ||
| ]) | ||
| } else { | ||
| self.notifyListeners("onInfoWindowClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "markerId": String(marker.hash.hashValue), | ||
| "latitude": marker.position.latitude, | ||
| "longitude": marker.position.longitude, | ||
| "title": marker.title ?? "", | ||
| "snippet": marker.snippet ?? "" | ||
| ]) | ||
| } | ||
| } | ||
| // onMyLocationButtonClick | ||
| public func didTapMyLocationButtonForMapView(for mapView: GMSMapView) -> Bool { | ||
| self.notifyListeners("onMyLocationButtonClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView) | ||
| ]) | ||
| return false | ||
| } | ||
| // onMyLocationClick | ||
| public func mapView(_ mapView: GMSMapView, didTapMyLocation location: CLLocationCoordinate2D) { | ||
| self.notifyListeners("onMyLocationButtonClick", data: [ | ||
| "mapId": self.findMapIdByMapView(mapView), | ||
| "latitude": location.latitude, | ||
| "longitude": location.longitude | ||
| ]) | ||
| } | ||
| } | ||
| // snippet from https://www.hackingwithswift.com/example-code/uicolor/how-to-convert-a-hex-color-to-a-uicolor | ||
| extension UIColor { | ||
| public convenience init?(hex: String) { | ||
| let r, g, b, a: CGFloat | ||
| if hex.hasPrefix("#") { | ||
| let start = hex.index(hex.startIndex, offsetBy: 1) | ||
| let hexColor = String(hex[start...]) | ||
| let scanner = Scanner(string: hexColor) | ||
| var hexNumber: UInt64 = 0 | ||
| if hexColor.count == 8 { | ||
| if scanner.scanHexInt64(&hexNumber) { | ||
| r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 | ||
| g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 | ||
| b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 | ||
| a = CGFloat(hexNumber & 0x000000ff) / 255 | ||
| self.init(red: r, green: g, blue: b, alpha: a) | ||
| return | ||
| } | ||
| } else { | ||
| if scanner.scanHexInt64(&hexNumber) { | ||
| r = CGFloat((hexNumber & 0xff0000) >> 16) / 255 | ||
| g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255 | ||
| b = CGFloat((hexNumber & 0x0000ff) >> 0) / 255 | ||
| self.init(red: r, green: g, blue: b, alpha: 1) | ||
| return | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct Circle { | ||
| let center: LatLng | ||
| let radius: Double | ||
| let strokeWidth: CGFloat | ||
| let strokeColor: UIColor | ||
| let fillColor: UIColor | ||
| let tappable: Bool? | ||
| let title: String? | ||
| let zIndex: Int32 | ||
| let tag: String? | ||
| init(from jsObject: JSObject) throws { | ||
| var strokeColor = UIColor.blue | ||
| var strokeWidth: CGFloat = 1.0 | ||
| var fillColor = UIColor.blue | ||
| guard let centerLatLng = jsObject["center"] as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("Circle object is missing the required 'center' property") | ||
| } | ||
| guard let lat = centerLatLng["lat"] as? Double, let lng = centerLatLng["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| guard let radius = jsObject["radius"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("Circle object is missing the required 'radius' property") | ||
| } | ||
| if let width = jsObject["strokeWeight"] as? Float { | ||
| strokeWidth = CGFloat(width) | ||
| } | ||
| let strokeOpacity = jsObject["strokeOpacity"] as? Double | ||
| if let hexColor = jsObject["strokeColor"] as? String { | ||
| strokeColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| strokeColor = strokeColor.withAlphaComponent(strokeOpacity ?? 1.0) | ||
| let fillOpacity = jsObject["fillOpacity"] as? Double | ||
| if let hexColor = jsObject["fillColor"] as? String { | ||
| fillColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| fillColor = fillColor.withAlphaComponent(fillOpacity ?? 1.0) | ||
| self.center = LatLng(lat: lat, lng: lng) | ||
| self.radius = radius | ||
| self.fillColor = fillColor | ||
| self.strokeColor = strokeColor | ||
| self.strokeWidth = strokeWidth | ||
| self.tag = jsObject["tag"] as? String | ||
| self.tappable = jsObject["clickable"] as? Bool | ||
| self.title = jsObject["title"] as? String | ||
| self.zIndex = Int32((jsObject["zIndex"] as? Int) ?? 0) | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct GoogleMapCameraConfig { | ||
| let coordinate: LatLng? | ||
| let zoom: Float? | ||
| let bearing: Double? | ||
| let angle: Double? | ||
| let animate: Bool? | ||
| let animationDuration: Double? | ||
| init(fromJSObject: JSObject) throws { | ||
| zoom = fromJSObject["zoom"] as? Float | ||
| bearing = fromJSObject["bearing"] as? Double | ||
| angle = fromJSObject["angle"] as? Double | ||
| animate = fromJSObject["animate"] as? Bool | ||
| animationDuration = fromJSObject["animationDuration"] as? Double | ||
| if let latLngObj = fromJSObject["coordinate"] as? JSObject { | ||
| guard let lat = latLngObj["lat"] as? Double, let lng = latLngObj["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| self.coordinate = LatLng(lat: lat, lng: lng) | ||
| } else { | ||
| self.coordinate = nil | ||
| } | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| import GoogleMaps | ||
| public struct GoogleMapConfig: Codable { | ||
| let width: Double | ||
| let height: Double | ||
| let x: Double | ||
| let y: Double | ||
| let center: LatLng | ||
| let zoom: Double | ||
| let styles: String? | ||
| var mapId: String? | ||
| let mapTypeId: String? | ||
| let maxZoom: Double? | ||
| let minZoom: Double? | ||
| let restriction: GoogleMapConfigRestriction? | ||
| let heading: Double? | ||
| init(fromJSObject: JSObject) throws { | ||
| guard let width = fromJSObject["width"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'width' property") | ||
| } | ||
| guard let height = fromJSObject["height"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'height' property") | ||
| } | ||
| guard let x = fromJSObject["x"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'x' property") | ||
| } | ||
| guard let y = fromJSObject["y"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'y' property") | ||
| } | ||
| guard let zoom = fromJSObject["zoom"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'zoom' property") | ||
| } | ||
| guard let latLngObj = fromJSObject["center"] as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfig object is missing the required 'center' property") | ||
| } | ||
| guard let lat = latLngObj["lat"] as? Double, let lng = latLngObj["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| self.width = round(width) | ||
| self.height = round(height) | ||
| self.x = x | ||
| self.y = y | ||
| self.center = LatLng(lat: lat, lng: lng) | ||
| if let stylesArray = fromJSObject["styles"] as? JSArray, let jsonData = try? JSONSerialization.data(withJSONObject: stylesArray, options: []) { | ||
| self.styles = String(data: jsonData, encoding: .utf8) | ||
| } else { | ||
| self.styles = nil | ||
| } | ||
| self.mapId = fromJSObject["iOSMapId"] as? String | ||
| self.mapTypeId = fromJSObject["mapTypeId"] as? String | ||
| var maxZoom = fromJSObject["maxZoom"] as? Double | ||
| var minZoom = fromJSObject["minZoom"] as? Double | ||
| if let unwrappedMinZoom = minZoom, let unwrappedMaxZoom = maxZoom, unwrappedMinZoom > unwrappedMaxZoom { | ||
| swap(&minZoom, &maxZoom) | ||
| } | ||
| self.minZoom = minZoom | ||
| self.maxZoom = maxZoom | ||
| if let maxZoom, zoom > maxZoom { | ||
| self.zoom = maxZoom | ||
| } else if let minZoom, zoom < minZoom { | ||
| self.zoom = minZoom | ||
| } else { | ||
| self.zoom = zoom | ||
| } | ||
| if let restrictionObj = fromJSObject["restriction"] as? JSObject { | ||
| self.restriction = try GoogleMapConfigRestriction(fromJSObject: restrictionObj) | ||
| } else { | ||
| self.restriction = nil | ||
| } | ||
| self.heading = fromJSObject["heading"] as? Double | ||
| } | ||
| } | ||
| public struct GoogleMapConfigRestriction: Codable { | ||
| let latLngBounds: GMSCoordinateBounds | ||
| init(fromJSObject: JSObject) throws { | ||
| guard let latLngBoundsObj = fromJSObject["latLngBounds"] as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds' property") | ||
| } | ||
| guard let north = latLngBoundsObj["north"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds.north' property") | ||
| } | ||
| guard let south = latLngBoundsObj["south"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds.south' property") | ||
| } | ||
| guard let east = latLngBoundsObj["east"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds.east' property") | ||
| } | ||
| guard let west = latLngBoundsObj["west"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("GoogleMapConfigRestriction object is missing the required 'latLngBounds.west' property") | ||
| } | ||
| let southWest = CLLocationCoordinate2D(latitude: south, longitude: west) | ||
| let northEast = CLLocationCoordinate2D(latitude: north, longitude: east) | ||
| self.latLngBounds = GMSCoordinateBounds(coordinate: southWest, coordinate: northEast) | ||
| } | ||
| enum CodingKeys: String, CodingKey { | ||
| case latLngBounds | ||
| } | ||
| struct LatLngBounds: Codable { | ||
| let north: CLLocationDegrees | ||
| let south: CLLocationDegrees | ||
| let east: CLLocationDegrees | ||
| let west: CLLocationDegrees | ||
| init(north: CLLocationDegrees, south: CLLocationDegrees, east: CLLocationDegrees, west: CLLocationDegrees) { | ||
| self.north = north | ||
| self.south = south | ||
| self.east = east | ||
| self.west = west | ||
| } | ||
| } | ||
| public func encode(to encoder: any Encoder) throws { | ||
| var container = encoder.container(keyedBy: CodingKeys.self) | ||
| let latLngBounds = LatLngBounds( | ||
| north: self.latLngBounds.northEast.latitude, | ||
| south: self.latLngBounds.southWest.latitude, | ||
| east: self.latLngBounds.northEast.longitude, | ||
| west: self.latLngBounds.southWest.longitude | ||
| ) | ||
| try container.encode(latLngBounds, forKey: .latLngBounds) | ||
| } | ||
| public init(from decoder: any Decoder) throws { | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| let latLngBounds = try container.decode(LatLngBounds.self, forKey: .latLngBounds) | ||
| let southWest = CLLocationCoordinate2D(latitude: latLngBounds.south, longitude: latLngBounds.west) | ||
| let northEast = CLLocationCoordinate2D(latitude: latLngBounds.north, longitude: latLngBounds.east) | ||
| self.latLngBounds = GMSCoordinateBounds(coordinate: southWest, coordinate: northEast) | ||
| } | ||
| } |
| import Foundation | ||
| public enum GoogleMapErrors: Error { | ||
| case invalidMapId | ||
| case mapNotFound | ||
| case markerNotFound | ||
| case invalidArguments(_ description: String) | ||
| case invalidAPIKey | ||
| case permissionsDeniedLocation | ||
| case unhandledError(_ description: String) | ||
| case tileOverlayNotFound | ||
| } | ||
| public struct GoogleMapErrorObject { | ||
| let extra: [String: Any]? | ||
| let code: Int | ||
| let message: String | ||
| init(_ code: Int, _ message: String, _ extra: [String: Any]? = nil) { | ||
| self.code = code | ||
| self.message = message | ||
| self.extra = extra | ||
| } | ||
| var asDictionary: [String: Any] { | ||
| return ["code": code, "message": message, "extra": extra ?? []] | ||
| } | ||
| } | ||
| public func getErrorObject(_ error: Error) -> GoogleMapErrorObject { | ||
| switch error { | ||
| case GoogleMapErrors.invalidMapId: | ||
| return GoogleMapErrorObject(1, "Missing or invalid map id.") | ||
| case GoogleMapErrors.mapNotFound: | ||
| return GoogleMapErrorObject(2, "Map not found for provided id.") | ||
| case GoogleMapErrors.markerNotFound: | ||
| return GoogleMapErrorObject(3, "Marker not found for provided id.") | ||
| case GoogleMapErrors.invalidArguments(let msg): | ||
| return GoogleMapErrorObject(4, "Invalid Arguments Provided: \(msg)") | ||
| case GoogleMapErrors.permissionsDeniedLocation: | ||
| return GoogleMapErrorObject(5, "Permissions denied for accessing device location.") | ||
| case GoogleMapErrors.invalidAPIKey: | ||
| return GoogleMapErrorObject(6, "Missing or invalid Google Maps SDK API key.") | ||
| case GoogleMapErrors.tileOverlayNotFound: | ||
| return GoogleMapErrorObject(7, "Tile overlay not found for provided id.") | ||
| case GoogleMapErrors.unhandledError(let msg): | ||
| return GoogleMapErrorObject(0, "Unhandled Error: \(msg)") | ||
| default: | ||
| return GoogleMapErrorObject(0, "Unhandled Error: \(error.localizedDescription)") | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct GoogleMapPadding { | ||
| let top: Float | ||
| let bottom: Float | ||
| let left: Float | ||
| let right: Float | ||
| init(fromJSObject: JSObject) throws { | ||
| top = fromJSObject["top"] as? Float ?? 0 | ||
| bottom = fromJSObject["bottom"] as? Float ?? 0 | ||
| left = fromJSObject["left"] as? Float ?? 0 | ||
| right = fromJSObject["right"] as? Float ?? 0 | ||
| } | ||
| } |
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>CFBundleDevelopmentRegion</key> | ||
| <string>$(DEVELOPMENT_LANGUAGE)</string> | ||
| <key>CFBundleExecutable</key> | ||
| <string>$(EXECUTABLE_NAME)</string> | ||
| <key>CFBundleIdentifier</key> | ||
| <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||
| <key>CFBundleInfoDictionaryVersion</key> | ||
| <string>6.0</string> | ||
| <key>CFBundleName</key> | ||
| <string>$(PRODUCT_NAME)</string> | ||
| <key>CFBundlePackageType</key> | ||
| <string>FMWK</string> | ||
| <key>CFBundleShortVersionString</key> | ||
| <string>1.0</string> | ||
| <key>CFBundleVersion</key> | ||
| <string>$(CURRENT_PROJECT_VERSION)</string> | ||
| <key>NSPrincipalClass</key> | ||
| <string></string> | ||
| </dict> | ||
| </plist> |
| import Foundation | ||
| import GoogleMaps | ||
| import Capacitor | ||
| import GoogleMapsUtils | ||
| public struct LatLng: Codable { | ||
| let lat: Double | ||
| let lng: Double | ||
| } | ||
| class GMViewController: UIViewController { | ||
| var mapViewBounds: [String: Double]! | ||
| var GMapView: GMSMapView! | ||
| var cameraPosition: [String: Double]! | ||
| var minimumClusterSize: Int? | ||
| var mapId: String? | ||
| private var clusterManager: GMUClusterManager? | ||
| var clusteringEnabled: Bool { | ||
| return clusterManager != nil | ||
| } | ||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
| let camera = GMSCameraPosition.camera(withLatitude: cameraPosition["latitude"] ?? 0, longitude: cameraPosition["longitude"] ?? 0, zoom: Float(cameraPosition["zoom"] ?? 12)) | ||
| let frame = CGRect(x: mapViewBounds["x"] ?? 0, y: mapViewBounds["y"] ?? 0, width: mapViewBounds["width"] ?? 0, height: mapViewBounds["height"] ?? 0) | ||
| if let id = mapId { | ||
| let gmsId = GMSMapID(identifier: id) | ||
| self.GMapView = GMSMapView(frame: frame, mapID: gmsId, camera: camera) | ||
| } else { | ||
| self.GMapView = GMSMapView(frame: frame, camera: camera) | ||
| } | ||
| self.view = GMapView | ||
| } | ||
| func initClusterManager(_ minClusterSize: Int?) { | ||
| let iconGenerator = GMUDefaultClusterIconGenerator() | ||
| let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm() | ||
| let renderer = GMUDefaultClusterRenderer(mapView: self.GMapView, clusterIconGenerator: iconGenerator) | ||
| self.minimumClusterSize = minClusterSize | ||
| if let minClusterSize = minClusterSize { | ||
| renderer.minimumClusterSize = UInt(minClusterSize) | ||
| } | ||
| self.clusterManager = GMUClusterManager(map: self.GMapView, algorithm: algorithm, renderer: renderer) | ||
| } | ||
| func destroyClusterManager() { | ||
| self.clusterManager = nil | ||
| } | ||
| func addMarkersToCluster(markers: [GMSMarker]) { | ||
| if let clusterManager = clusterManager { | ||
| clusterManager.add(markers) | ||
| clusterManager.cluster() | ||
| } | ||
| } | ||
| func removeMarkersFromCluster(markers: [GMSMarker]) { | ||
| if let clusterManager = clusterManager { | ||
| markers.forEach { marker in | ||
| clusterManager.remove(marker) | ||
| } | ||
| clusterManager.cluster() | ||
| } | ||
| } | ||
| } | ||
| // swiftlint:disable type_body_length | ||
| public class Map { | ||
| var id: String | ||
| var config: GoogleMapConfig | ||
| var mapViewController: GMViewController | ||
| var targetViewController: UIView? | ||
| var markers = [Int: GMSMarker]() | ||
| var tileOverlays = [Int: GMSURLTileLayer]() | ||
| var polygons = [Int: GMSPolygon]() | ||
| var circles = [Int: GMSCircle]() | ||
| var polylines = [Int: GMSPolyline]() | ||
| var markerIcons = [String: UIImage]() | ||
| // swiftlint:disable identifier_name | ||
| public static let MAP_TAG = 99999 | ||
| // swiftlint:enable identifier_name | ||
| // swiftlint:disable weak_delegate | ||
| private var delegate: CapacitorGoogleMapsPlugin | ||
| init(id: String, config: GoogleMapConfig, delegate: CapacitorGoogleMapsPlugin) { | ||
| self.id = id | ||
| self.config = config | ||
| self.delegate = delegate | ||
| self.mapViewController = GMViewController() | ||
| self.mapViewController.mapId = config.mapId | ||
| self.render() | ||
| } | ||
| func render() { | ||
| DispatchQueue.main.async { | ||
| self.mapViewController.mapViewBounds = [ | ||
| "width": self.config.width, | ||
| "height": self.config.height, | ||
| "x": self.config.x, | ||
| "y": self.config.y | ||
| ] | ||
| self.mapViewController.cameraPosition = [ | ||
| "latitude": self.config.center.lat, | ||
| "longitude": self.config.center.lng, | ||
| "zoom": self.config.zoom | ||
| ] | ||
| self.targetViewController = self.getTargetContainer(refWidth: self.config.width, refHeight: self.config.height) | ||
| if let target = self.targetViewController { | ||
| target.tag = Map.MAP_TAG | ||
| target.removeAllSubview() | ||
| self.mapViewController.view.frame = target.bounds | ||
| target.addSubview(self.mapViewController.view) | ||
| self.mapViewController.GMapView.delegate = self.delegate | ||
| } | ||
| if let styles = self.config.styles { | ||
| do { | ||
| self.mapViewController.GMapView.mapStyle = try GMSMapStyle(jsonString: styles) | ||
| } catch { | ||
| CAPLog.print("Invalid Google Maps styles") | ||
| } | ||
| } | ||
| let minZoom = self.config.minZoom.map { Float($0) } ?? self.mapViewController.GMapView.minZoom | ||
| let maxZoom = self.config.maxZoom.map { Float($0) } ?? self.mapViewController.GMapView.maxZoom | ||
| self.mapViewController.GMapView.setMinZoom(minZoom, maxZoom: maxZoom) | ||
| if let mapTypeId = self.config.mapTypeId { | ||
| switch mapTypeId { | ||
| case "hybrid": | ||
| self.mapViewController.GMapView.mapType = .hybrid | ||
| case "roadmap": | ||
| self.mapViewController.GMapView.mapType = .normal | ||
| case "satellite": | ||
| self.mapViewController.GMapView.mapType = .satellite | ||
| case "terrain": | ||
| self.mapViewController.GMapView.mapType = .terrain | ||
| default: | ||
| break | ||
| } | ||
| } | ||
| if let restriction = self.config.restriction { | ||
| self.mapViewController.GMapView.cameraTargetBounds = restriction.latLngBounds | ||
| } | ||
| if let heading = self.config.heading { | ||
| self.mapViewController.GMapView.animate(toBearing: heading) | ||
| } | ||
| self.delegate.notifyListeners("onMapReady", data: [ | ||
| "mapId": self.id | ||
| ]) | ||
| } | ||
| } | ||
| func updateRender(mapBounds: CGRect) { | ||
| DispatchQueue.main.sync { | ||
| let newWidth = round(Double(mapBounds.width)) | ||
| let newHeight = round(Double(mapBounds.height)) | ||
| let isWidthEqual = round(Double(self.mapViewController.view.bounds.width)) == newWidth | ||
| let isHeightEqual = round(Double(self.mapViewController.view.bounds.height)) == newHeight | ||
| if !isWidthEqual || !isHeightEqual { | ||
| CATransaction.begin() | ||
| CATransaction.setDisableActions(true) | ||
| self.mapViewController.view.frame.size.width = newWidth | ||
| self.mapViewController.view.frame.size.height = newHeight | ||
| CATransaction.commit() | ||
| } | ||
| } | ||
| } | ||
| func rebindTargetContainer(mapBounds: CGRect) { | ||
| DispatchQueue.main.sync { | ||
| if let target = self.getTargetContainer(refWidth: round(Double(mapBounds.width)), refHeight: round(Double(mapBounds.height))) { | ||
| self.targetViewController = target | ||
| target.tag = Map.MAP_TAG | ||
| target.removeAllSubview() | ||
| CATransaction.begin() | ||
| CATransaction.setDisableActions(true) | ||
| self.mapViewController.view.frame.size.width = mapBounds.width | ||
| self.mapViewController.view.frame.size.height = mapBounds.height | ||
| CATransaction.commit() | ||
| target.addSubview(self.mapViewController.view) | ||
| } | ||
| } | ||
| } | ||
| private func getTargetContainer(refWidth: Double, refHeight: Double) -> UIView? { | ||
| if let bridge = self.delegate.bridge { | ||
| for item in bridge.webView!.getAllSubViews() { | ||
| let isScrollView = item.isKind(of: NSClassFromString("WKChildScrollView")!) || item.isKind(of: NSClassFromString("WKScrollView")!) | ||
| let isBridgeScrollView = item.isEqual(bridge.webView?.scrollView) | ||
| if isScrollView && !isBridgeScrollView { | ||
| (item as? UIScrollView)?.isScrollEnabled = true | ||
| let height = Double((item as? UIScrollView)?.contentSize.height ?? 0) | ||
| let width = Double((item as? UIScrollView)?.contentSize.width ?? 0) | ||
| let actualHeight = round(height / 2) | ||
| let isWidthEqual = width == self.config.width | ||
| let isHeightEqual = actualHeight == self.config.height | ||
| if isWidthEqual && isHeightEqual && item.tag < self.targetViewController?.tag ?? Map.MAP_TAG { | ||
| return item | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| func destroy() { | ||
| DispatchQueue.main.async { | ||
| self.mapViewController.GMapView = nil | ||
| self.targetViewController?.tag = 0 | ||
| self.mapViewController.view = nil | ||
| self.enableTouch() | ||
| } | ||
| } | ||
| func enableTouch() { | ||
| DispatchQueue.main.async { | ||
| if let target = self.targetViewController, let itemIndex = WKWebView.disabledTargets.firstIndex(of: target) { | ||
| WKWebView.disabledTargets.remove(at: itemIndex) | ||
| } | ||
| } | ||
| } | ||
| func addTileOverlay(tileOverlay: TileOverlay) throws -> Int { | ||
| var tileOverlayHash = 0 | ||
| DispatchQueue.main.sync { | ||
| let urlConstructor: GMSTileURLConstructor = { x, y, zoom in | ||
| URL(string: tileOverlay.url | ||
| .replacingOccurrences(of: "{x}", with: "\(x)") | ||
| .replacingOccurrences(of: "{y}", with: "\(y)") | ||
| .replacingOccurrences(of: "{z}", with: "\(zoom)") | ||
| ) | ||
| } | ||
| let layer = GMSURLTileLayer(urlConstructor: urlConstructor) | ||
| layer.opacity = tileOverlay.opacity ?? 1 | ||
| layer.zIndex = tileOverlay.zIndex | ||
| layer.map = self.mapViewController.GMapView | ||
| self.tileOverlays[layer.hash.hashValue] = layer | ||
| tileOverlayHash = layer.hash.hashValue | ||
| } | ||
| return tileOverlayHash | ||
| } | ||
| func removeTileOverlay(id: Int) throws { | ||
| if let tileOverlay = self.tileOverlays[id] { | ||
| DispatchQueue.main.async { | ||
| tileOverlay.map = nil | ||
| self.tileOverlays.removeValue(forKey: id) | ||
| } | ||
| } else { | ||
| throw GoogleMapErrors.tileOverlayNotFound | ||
| } | ||
| } | ||
| func disableTouch() { | ||
| DispatchQueue.main.async { | ||
| if let target = self.targetViewController, !WKWebView.disabledTargets.contains(target) { | ||
| WKWebView.disabledTargets.append(target) | ||
| } | ||
| } | ||
| } | ||
| func addMarker(marker: Marker) throws -> Int { | ||
| var markerHash = 0 | ||
| DispatchQueue.main.sync { | ||
| let newMarker = self.buildMarker(marker: marker) | ||
| if self.mapViewController.clusteringEnabled { | ||
| self.mapViewController.addMarkersToCluster(markers: [newMarker]) | ||
| } else { | ||
| newMarker.map = self.mapViewController.GMapView | ||
| } | ||
| self.markers[newMarker.hash.hashValue] = newMarker | ||
| markerHash = newMarker.hash.hashValue | ||
| } | ||
| return markerHash | ||
| } | ||
| func addMarkers(markers: [Marker]) throws -> [Int] { | ||
| var markerHashes: [Int] = [] | ||
| DispatchQueue.main.sync { | ||
| var googleMapsMarkers: [GMSMarker] = [] | ||
| markers.forEach { marker in | ||
| let newMarker = self.buildMarker(marker: marker) | ||
| if self.mapViewController.clusteringEnabled { | ||
| googleMapsMarkers.append(newMarker) | ||
| } else { | ||
| newMarker.map = self.mapViewController.GMapView | ||
| } | ||
| self.markers[newMarker.hash.hashValue] = newMarker | ||
| markerHashes.append(newMarker.hash.hashValue) | ||
| } | ||
| if self.mapViewController.clusteringEnabled { | ||
| self.mapViewController.addMarkersToCluster(markers: googleMapsMarkers) | ||
| } | ||
| } | ||
| return markerHashes | ||
| } | ||
| func addPolygons(polygons: [Polygon]) throws -> [Int] { | ||
| var polygonHashes: [Int] = [] | ||
| DispatchQueue.main.sync { | ||
| polygons.forEach { polygon in | ||
| let newPolygon = self.buildPolygon(polygon: polygon) | ||
| newPolygon.map = self.mapViewController.GMapView | ||
| self.polygons[newPolygon.hash.hashValue] = newPolygon | ||
| polygonHashes.append(newPolygon.hash.hashValue) | ||
| } | ||
| } | ||
| return polygonHashes | ||
| } | ||
| func addCircles(circles: [Circle]) throws -> [Int] { | ||
| var circleHashes: [Int] = [] | ||
| DispatchQueue.main.sync { | ||
| circles.forEach { circle in | ||
| let newCircle = self.buildCircle(circle: circle) | ||
| newCircle.map = self.mapViewController.GMapView | ||
| self.circles[newCircle.hash.hashValue] = newCircle | ||
| circleHashes.append(newCircle.hash.hashValue) | ||
| } | ||
| } | ||
| return circleHashes | ||
| } | ||
| func addPolylines(lines: [Polyline]) throws -> [Int] { | ||
| var polylineHashes: [Int] = [] | ||
| DispatchQueue.main.sync { | ||
| lines.forEach { line in | ||
| let newLine = self.buildPolyline(line: line) | ||
| newLine.map = self.mapViewController.GMapView | ||
| self.polylines[newLine.hash.hashValue] = newLine | ||
| polylineHashes.append(newLine.hash.hashValue) | ||
| } | ||
| } | ||
| return polylineHashes | ||
| } | ||
| func enableClustering(_ minClusterSize: Int?) { | ||
| if !self.mapViewController.clusteringEnabled { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.initClusterManager(minClusterSize) | ||
| // add existing markers to the cluster | ||
| if !self.markers.isEmpty { | ||
| var existingMarkers: [GMSMarker] = [] | ||
| for (_, marker) in self.markers { | ||
| marker.map = nil | ||
| existingMarkers.append(marker) | ||
| } | ||
| self.mapViewController.addMarkersToCluster(markers: existingMarkers) | ||
| } | ||
| } | ||
| } else if self.mapViewController.minimumClusterSize != minClusterSize { | ||
| self.mapViewController.destroyClusterManager() | ||
| enableClustering(minClusterSize) | ||
| } | ||
| } | ||
| func disableClustering() { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.destroyClusterManager() | ||
| // add existing markers back to the map | ||
| if !self.markers.isEmpty { | ||
| for (_, marker) in self.markers { | ||
| marker.map = self.mapViewController.GMapView | ||
| } | ||
| } | ||
| } | ||
| } | ||
| func removeMarker(id: Int) throws { | ||
| if let marker = self.markers[id] { | ||
| DispatchQueue.main.async { | ||
| if self.mapViewController.clusteringEnabled { | ||
| self.mapViewController.removeMarkersFromCluster(markers: [marker]) | ||
| } | ||
| marker.map = nil | ||
| self.markers.removeValue(forKey: id) | ||
| } | ||
| } else { | ||
| throw GoogleMapErrors.markerNotFound | ||
| } | ||
| } | ||
| func removePolygons(ids: [Int]) throws { | ||
| DispatchQueue.main.sync { | ||
| ids.forEach { id in | ||
| if let polygon = self.polygons[id] { | ||
| polygon.map = nil | ||
| self.polygons.removeValue(forKey: id) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| func removeCircles(ids: [Int]) throws { | ||
| DispatchQueue.main.sync { | ||
| ids.forEach { id in | ||
| if let circle = self.circles[id] { | ||
| circle.map = nil | ||
| self.circles.removeValue(forKey: id) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| func removePolylines(ids: [Int]) throws { | ||
| DispatchQueue.main.sync { | ||
| ids.forEach { id in | ||
| if let line = self.polylines[id] { | ||
| line.map = nil | ||
| self.polylines.removeValue(forKey: id) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| func setCamera(config: GoogleMapCameraConfig) throws { | ||
| let currentCamera = self.mapViewController.GMapView.camera | ||
| let lat = config.coordinate?.lat ?? currentCamera.target.latitude | ||
| let lng = config.coordinate?.lng ?? currentCamera.target.longitude | ||
| let zoom = config.zoom ?? currentCamera.zoom | ||
| let bearing = config.bearing ?? Double(currentCamera.bearing) | ||
| let angle = config.angle ?? currentCamera.viewingAngle | ||
| let animate = config.animate ?? false | ||
| DispatchQueue.main.sync { | ||
| let newCamera = GMSCameraPosition(latitude: lat, longitude: lng, zoom: zoom, bearing: bearing, viewingAngle: angle) | ||
| if animate { | ||
| self.mapViewController.GMapView.animate(to: newCamera) | ||
| } else { | ||
| self.mapViewController.GMapView.camera = newCamera | ||
| } | ||
| } | ||
| } | ||
| func getMapType() -> GMSMapViewType { | ||
| return self.mapViewController.GMapView.mapType | ||
| } | ||
| func setMapType(mapType: GMSMapViewType) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.mapType = mapType | ||
| } | ||
| } | ||
| func enableIndoorMaps(enabled: Bool) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.isIndoorEnabled = enabled | ||
| } | ||
| } | ||
| func enableTrafficLayer(enabled: Bool) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.isTrafficEnabled = enabled | ||
| } | ||
| } | ||
| func enableAccessibilityElements(enabled: Bool) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.accessibilityElementsHidden = enabled | ||
| } | ||
| } | ||
| func enableCurrentLocation(enabled: Bool) throws { | ||
| DispatchQueue.main.sync { | ||
| self.mapViewController.GMapView.isMyLocationEnabled = enabled | ||
| } | ||
| } | ||
| func setPadding(padding: GoogleMapPadding) throws { | ||
| DispatchQueue.main.sync { | ||
| let mapInsets = UIEdgeInsets(top: CGFloat(padding.top), left: CGFloat(padding.left), bottom: CGFloat(padding.bottom), right: CGFloat(padding.right)) | ||
| self.mapViewController.GMapView.padding = mapInsets | ||
| } | ||
| } | ||
| func removeMarkers(ids: [Int]) throws { | ||
| DispatchQueue.main.sync { | ||
| var markers: [GMSMarker] = [] | ||
| ids.forEach { id in | ||
| if let marker = self.markers[id] { | ||
| marker.map = nil | ||
| self.markers.removeValue(forKey: id) | ||
| markers.append(marker) | ||
| } | ||
| } | ||
| if self.mapViewController.clusteringEnabled { | ||
| self.mapViewController.removeMarkersFromCluster(markers: markers) | ||
| } | ||
| } | ||
| } | ||
| func getMapLatLngBounds() -> GMSCoordinateBounds? { | ||
| return GMSCoordinateBounds(region: self.mapViewController.GMapView.projection.visibleRegion()) | ||
| } | ||
| func fitBounds(bounds: GMSCoordinateBounds, padding: CGFloat) { | ||
| DispatchQueue.main.sync { | ||
| let cameraUpdate = GMSCameraUpdate.fit(bounds, withPadding: padding) | ||
| self.mapViewController.GMapView.animate(with: cameraUpdate) | ||
| } | ||
| } | ||
| private func getFrameOverflowBounds(frame: CGRect, mapBounds: CGRect) -> [CGRect] { | ||
| var intersections: [CGRect] = [] | ||
| // get top overflow | ||
| if mapBounds.origin.y < frame.origin.y { | ||
| let height = frame.origin.y - mapBounds.origin.y | ||
| let width = mapBounds.width | ||
| intersections.append(CGRect(x: 0, y: 0, width: width, height: height)) | ||
| } | ||
| // get bottom overflow | ||
| if (mapBounds.origin.y + mapBounds.height) > (frame.origin.y + frame.height) { | ||
| let height = (mapBounds.origin.y + mapBounds.height) - (frame.origin.y + frame.height) | ||
| let width = mapBounds.width | ||
| intersections.append(CGRect(x: 0, y: mapBounds.height, width: width, height: height)) | ||
| } | ||
| return intersections | ||
| } | ||
| private func buildCircle(circle: Circle) -> GMSCircle { | ||
| let newCircle = GMSCircle() | ||
| newCircle.title = circle.title | ||
| newCircle.strokeColor = circle.strokeColor | ||
| newCircle.strokeWidth = circle.strokeWidth | ||
| newCircle.fillColor = circle.fillColor | ||
| newCircle.position = CLLocationCoordinate2D(latitude: circle.center.lat, longitude: circle.center.lng) | ||
| newCircle.radius = CLLocationDistance(circle.radius) | ||
| newCircle.isTappable = circle.tappable ?? false | ||
| newCircle.zIndex = circle.zIndex | ||
| newCircle.userData = circle.tag | ||
| return newCircle | ||
| } | ||
| private func buildPolygon(polygon: Polygon) -> GMSPolygon { | ||
| let newPolygon = GMSPolygon() | ||
| newPolygon.title = polygon.title | ||
| newPolygon.strokeColor = polygon.strokeColor | ||
| newPolygon.strokeWidth = polygon.strokeWidth | ||
| newPolygon.fillColor = polygon.fillColor | ||
| newPolygon.isTappable = polygon.tappable ?? false | ||
| newPolygon.geodesic = polygon.geodesic ?? false | ||
| newPolygon.zIndex = polygon.zIndex | ||
| newPolygon.userData = polygon.tag | ||
| var shapeIndex = 0 | ||
| let outerShape = GMSMutablePath() | ||
| var holes: [GMSMutablePath] = [] | ||
| polygon.shapes.forEach { shape in | ||
| if shapeIndex == 0 { | ||
| shape.forEach { coord in | ||
| outerShape.add(CLLocationCoordinate2D(latitude: coord.lat, longitude: coord.lng)) | ||
| } | ||
| } else { | ||
| let holeShape = GMSMutablePath() | ||
| shape.forEach { coord in | ||
| holeShape.add(CLLocationCoordinate2D(latitude: coord.lat, longitude: coord.lng)) | ||
| } | ||
| holes.append(holeShape) | ||
| } | ||
| shapeIndex += 1 | ||
| } | ||
| newPolygon.path = outerShape | ||
| newPolygon.holes = holes | ||
| return newPolygon | ||
| } | ||
| private func buildPolyline(line: Polyline) -> GMSPolyline { | ||
| let newPolyline = GMSPolyline() | ||
| newPolyline.title = line.title | ||
| newPolyline.strokeColor = line.strokeColor | ||
| newPolyline.strokeWidth = line.strokeWidth | ||
| newPolyline.isTappable = line.tappable ?? false | ||
| newPolyline.geodesic = line.geodesic ?? false | ||
| newPolyline.zIndex = line.zIndex | ||
| newPolyline.userData = line.tag | ||
| let path = GMSMutablePath() | ||
| line.path.forEach { coord in | ||
| path.add(CLLocationCoordinate2D(latitude: coord.lat, longitude: coord.lng)) | ||
| } | ||
| newPolyline.path = path | ||
| if line.styleSpans.count > 0 { | ||
| var spans: [GMSStyleSpan] = [] | ||
| line.styleSpans.forEach { span in | ||
| if let segments = span.segments { | ||
| spans.append(GMSStyleSpan(color: span.color, segments: segments)) | ||
| } else { | ||
| spans.append(GMSStyleSpan(color: span.color)) | ||
| } | ||
| } | ||
| newPolyline.spans = spans | ||
| } | ||
| return newPolyline | ||
| } | ||
| private func buildMarker(marker: Marker) -> GMSMarker { | ||
| let newMarker = GMSMarker() | ||
| newMarker.position = CLLocationCoordinate2D(latitude: marker.coordinate.lat, longitude: marker.coordinate.lng) | ||
| newMarker.title = marker.title | ||
| newMarker.snippet = marker.snippet | ||
| newMarker.isFlat = marker.isFlat ?? false | ||
| newMarker.opacity = marker.opacity ?? 1 | ||
| newMarker.isDraggable = marker.draggable ?? false | ||
| newMarker.zIndex = marker.zIndex | ||
| if let iconAnchor = marker.iconAnchor { | ||
| newMarker.groundAnchor = iconAnchor | ||
| } | ||
| // cache and reuse marker icon uiimages | ||
| if let iconUrl = marker.iconUrl { | ||
| if let iconImage = self.markerIcons[iconUrl] { | ||
| newMarker.icon = getResizedIcon(iconImage, marker) | ||
| } else { | ||
| if iconUrl.starts(with: "https:") { | ||
| if let url = URL(string: iconUrl) { | ||
| URLSession.shared.dataTask(with: url) { (data, _, _) in | ||
| DispatchQueue.main.async { | ||
| if let data = data, let iconImage = UIImage(data: data) { | ||
| self.markerIcons[iconUrl] = iconImage | ||
| newMarker.icon = getResizedIcon(iconImage, marker) | ||
| } | ||
| } | ||
| }.resume() | ||
| } | ||
| } else if let iconImage = UIImage(named: "public/\(iconUrl)") { | ||
| self.markerIcons[iconUrl] = iconImage | ||
| newMarker.icon = getResizedIcon(iconImage, marker) | ||
| } else { | ||
| var detailedMessage = "" | ||
| if iconUrl.hasSuffix(".svg") { | ||
| detailedMessage = "SVG not supported." | ||
| } | ||
| print("CapacitorGoogleMaps Warning: could not load image '\(iconUrl)'. \(detailedMessage) Using default marker icon.") | ||
| } | ||
| } | ||
| } else { | ||
| if let color = marker.color { | ||
| newMarker.icon = GMSMarker.markerImage(with: color) | ||
| } | ||
| } | ||
| return newMarker | ||
| } | ||
| } | ||
| private func getResizedIcon(_ iconImage: UIImage, _ marker: Marker) -> UIImage? { | ||
| if let iconSize = marker.iconSize { | ||
| return iconImage.resizeImageTo(size: iconSize) | ||
| } else { | ||
| return iconImage | ||
| } | ||
| } | ||
| extension WKWebView { | ||
| static var disabledTargets: [UIView] = [] | ||
| override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { | ||
| var hitView = super.hitTest(point, with: event) | ||
| if let tempHitView = hitView, WKWebView.disabledTargets.contains(tempHitView) { | ||
| return nil | ||
| } | ||
| if let typeClass = NSClassFromString("WKChildScrollView"), let tempHitView = hitView, tempHitView.isKind(of: typeClass) { | ||
| for item in tempHitView.subviews.reversed() { | ||
| let convertPoint = item.convert(point, from: self) | ||
| if let hitTestView = item.hitTest(convertPoint, with: event) { | ||
| hitView = hitTestView | ||
| break | ||
| } | ||
| } | ||
| } | ||
| return hitView | ||
| } | ||
| } | ||
| extension UIView { | ||
| private static var allSubviews: [UIView] = [] | ||
| private func viewArray(root: UIView) -> [UIView] { | ||
| var index = root.tag | ||
| for view in root.subviews { | ||
| if view.tag == Map.MAP_TAG { | ||
| // view already in use as in map | ||
| continue | ||
| } | ||
| // tag the index depth of the uiview | ||
| view.tag = index | ||
| if view.isKind(of: UIView.self) { | ||
| UIView.allSubviews.append(view) | ||
| } | ||
| _ = viewArray(root: view) | ||
| index += 1 | ||
| } | ||
| return UIView.allSubviews | ||
| } | ||
| fileprivate func getAllSubViews() -> [UIView] { | ||
| UIView.allSubviews = [] | ||
| return viewArray(root: self).reversed() | ||
| } | ||
| fileprivate func removeAllSubview() { | ||
| subviews.forEach { | ||
| $0.removeFromSuperview() | ||
| } | ||
| } | ||
| } | ||
| extension UIImage { | ||
| func resizeImageTo(size: CGSize) -> UIImage? { | ||
| UIGraphicsBeginImageContextWithOptions(size, false, 0.0) | ||
| self.draw(in: CGRect(origin: CGPoint.zero, size: size)) | ||
| let resizedImage = UIGraphicsGetImageFromCurrentImageContext()! | ||
| UIGraphicsEndImageContext() | ||
| return resizedImage | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct Marker { | ||
| let coordinate: LatLng | ||
| let opacity: Float? | ||
| let title: String? | ||
| let snippet: String? | ||
| let isFlat: Bool? | ||
| let iconUrl: String? | ||
| let iconSize: CGSize? | ||
| let iconAnchor: CGPoint? | ||
| let draggable: Bool? | ||
| let color: UIColor? | ||
| let zIndex: Int32 | ||
| init(fromJSObject: JSObject) throws { | ||
| guard let latLngObj = fromJSObject["coordinate"] as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("Marker object is missing the required 'coordinate' property") | ||
| } | ||
| guard let lat = latLngObj["lat"] as? Double, let lng = latLngObj["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| var iconSize: CGSize? | ||
| if let sizeObj = fromJSObject["iconSize"] as? JSObject { | ||
| if let width = sizeObj["width"] as? Double, let height = sizeObj["height"] as? Double { | ||
| iconSize = CGSize(width: width, height: height) | ||
| } | ||
| } | ||
| var iconAnchor: CGPoint? | ||
| if let anchorObject = fromJSObject["iconAnchor"] as? JSObject { | ||
| if let x = anchorObject["x"] as? Double, let y = anchorObject["y"] as? Double { | ||
| if let size = iconSize { | ||
| let u = x / size.width | ||
| let v = y / size.height | ||
| iconAnchor = CGPoint(x: u, y: v) | ||
| } | ||
| } | ||
| } | ||
| var tintColor: UIColor? | ||
| if let rgbObject = fromJSObject["tintColor"] as? JSObject { | ||
| if let r = rgbObject["r"] as? Double, let g = rgbObject["g"] as? Double, let b = rgbObject["b"] as? Double, let a = rgbObject["a"] as? Double { | ||
| let uiColorR = CGFloat(r / 255).clamp(min: 0, max: 255) | ||
| let uiColorG = CGFloat(g / 255).clamp(min: 0, max: 255) | ||
| let uiColorB = CGFloat(b / 255).clamp(min: 0, max: 255) | ||
| tintColor = UIColor(red: uiColorR, green: uiColorG, blue: uiColorB, alpha: CGFloat(a)) | ||
| } | ||
| } | ||
| self.coordinate = LatLng(lat: lat, lng: lng) | ||
| self.opacity = fromJSObject["opacity"] as? Float | ||
| self.title = fromJSObject["title"] as? String | ||
| self.snippet = fromJSObject["snippet"] as? String | ||
| self.isFlat = fromJSObject["isFlat"] as? Bool | ||
| self.iconUrl = fromJSObject["iconUrl"] as? String | ||
| self.draggable = fromJSObject["draggable"] as? Bool | ||
| self.iconSize = iconSize | ||
| self.iconAnchor = iconAnchor | ||
| self.color = tintColor | ||
| self.zIndex = Int32((fromJSObject["zIndex"] as? Int) ?? 0) | ||
| } | ||
| } | ||
| extension CGFloat { | ||
| func clamp(min: CGFloat, max: CGFloat) -> CGFloat { | ||
| if self < min { | ||
| return min | ||
| } | ||
| if self > max { | ||
| return max | ||
| } | ||
| return self | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct Polygon { | ||
| let shapes: [[LatLng]] | ||
| let strokeWidth: CGFloat | ||
| let strokeColor: UIColor | ||
| let fillColor: UIColor | ||
| let tappable: Bool? | ||
| let geodesic: Bool? | ||
| let title: String? | ||
| let zIndex: Int32 | ||
| let tag: String? | ||
| init(fromJSObject: JSObject) throws { | ||
| var strokeColor = UIColor.blue | ||
| var strokeWidth: CGFloat = 1.0 | ||
| var fillColor = UIColor.blue | ||
| var processedShapes: [[LatLng]] = [] | ||
| if let width = fromJSObject["strokeWeight"] as? Float { | ||
| strokeWidth = CGFloat(width) | ||
| } | ||
| let strokeOpacity = fromJSObject["strokeOpacity"] as? Double | ||
| if let hexColor = fromJSObject["strokeColor"] as? String { | ||
| strokeColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| strokeColor = strokeColor.withAlphaComponent(strokeOpacity ?? 1.0) | ||
| let fillOpacity = fromJSObject["fillOpacity"] as? Double | ||
| if let hexColor = fromJSObject["fillColor"] as? String { | ||
| fillColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| fillColor = fillColor.withAlphaComponent(fillOpacity ?? 1.0) | ||
| guard let shapeJSArray = fromJSObject["paths"] as? JSArray else { | ||
| throw GoogleMapErrors.invalidArguments("Polygon object is missing the required 'paths' property") | ||
| } | ||
| if let obj = shapeJSArray.first, obj as? JSArray != nil { | ||
| try shapeJSArray.forEach({ obj in | ||
| if let shapeArr = obj as? JSArray { | ||
| try processedShapes.append(Polygon.processShape(shapeArr)) | ||
| } | ||
| }) | ||
| } else { | ||
| // is a single shape | ||
| try processedShapes.append(Polygon.processShape(shapeJSArray)) | ||
| } | ||
| self.shapes = processedShapes | ||
| self.fillColor = fillColor | ||
| self.strokeColor = strokeColor | ||
| self.strokeWidth = strokeWidth | ||
| self.tag = fromJSObject["tag"] as? String | ||
| self.tappable = fromJSObject["clickable"] as? Bool | ||
| self.title = fromJSObject["title"] as? String | ||
| self.geodesic = fromJSObject["geodesic"] as? Bool | ||
| self.zIndex = Int32((fromJSObject["zIndex"] as? Int) ?? 0) | ||
| } | ||
| private static func processShape(_ shapeArr: JSArray) throws -> [LatLng] { | ||
| var shape: [LatLng] = [] | ||
| try shapeArr.forEach { obj in | ||
| guard let jsCoord = obj as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| guard let lat = jsCoord["lat"] as? Double, let lng = jsCoord["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| shape.append(LatLng(lat: lat, lng: lng)) | ||
| } | ||
| return shape | ||
| } | ||
| } |
| import Foundation | ||
| import Capacitor | ||
| public struct StyleSpan { | ||
| let color: UIColor | ||
| let segments: Double? | ||
| } | ||
| public struct Polyline { | ||
| let path: [LatLng] | ||
| let strokeWidth: CGFloat | ||
| let strokeColor: UIColor | ||
| let title: String? | ||
| let tappable: Bool? | ||
| let geodesic: Bool? | ||
| let zIndex: Int32 | ||
| let tag: String? | ||
| let styleSpans: [StyleSpan] | ||
| init(fromJSObject: JSObject) throws { | ||
| var strokeColor = UIColor.blue | ||
| var strokeWidth: CGFloat = 1.0 | ||
| var path: [LatLng] = [] | ||
| var styleSpans: [StyleSpan] = [] | ||
| if let width = fromJSObject["strokeWeight"] as? Float { | ||
| strokeWidth = CGFloat(width) | ||
| } | ||
| let strokeOpacity = fromJSObject["strokeOpacity"] as? Double | ||
| if let hexColor = fromJSObject["strokeColor"] as? String { | ||
| strokeColor = UIColor(hex: hexColor) ?? UIColor.blue | ||
| } | ||
| strokeColor = strokeColor.withAlphaComponent(strokeOpacity ?? 1.0) | ||
| guard let pathJSArray = fromJSObject["path"] as? JSArray else { | ||
| throw GoogleMapErrors.invalidArguments("Polyline object is missing the required 'path' property") | ||
| } | ||
| try pathJSArray.forEach { obj in | ||
| guard let jsCoord = obj as? JSObject else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| guard let lat = jsCoord["lat"] as? Double, let lng = jsCoord["lng"] as? Double else { | ||
| throw GoogleMapErrors.invalidArguments("LatLng object is missing the required 'lat' and/or 'lng' property") | ||
| } | ||
| path.append(LatLng(lat: lat, lng: lng)) | ||
| } | ||
| if let styleSpanJSArray = fromJSObject["styleSpans"] as? JSArray { | ||
| styleSpanJSArray.forEach({ obj in | ||
| if let styleSpanObj = obj as? JSObject, | ||
| let hexColor = styleSpanObj["color"] as? String, | ||
| let color = UIColor(hex: hexColor) { | ||
| let segments = styleSpanObj["segments"] as? Double | ||
| styleSpans.append(StyleSpan(color: color, segments: segments)) | ||
| } | ||
| }) | ||
| } | ||
| self.strokeColor = strokeColor | ||
| self.strokeWidth = strokeWidth | ||
| self.tag = fromJSObject["tag"] as? String | ||
| self.title = fromJSObject["title"] as? String | ||
| self.tappable = fromJSObject["clickable"] as? Bool | ||
| self.geodesic = fromJSObject["geodesic"] as? Bool | ||
| self.zIndex = Int32((fromJSObject["zIndex"] as? Int) ?? 0) | ||
| self.path = path | ||
| self.styleSpans = styleSpans | ||
| } | ||
| } |
| import Capacitor | ||
| public struct TileOverlay: Codable { | ||
| let url: String | ||
| let opacity: Float? | ||
| let visible: Bool? | ||
| let zIndex: Int32 | ||
| init(fromJSObject: JSObject) throws { | ||
| guard let url = fromJSObject["url"] as? String else { | ||
| throw GoogleMapErrors.invalidArguments("TileOverlay object is missing the required 'url' property") | ||
| } | ||
| self.url = url | ||
| self.opacity = fromJSObject["opacity"] as? Float | ||
| self.visible = fromJSObject["isFlat"] as? Bool | ||
| self.zIndex = Int32((fromJSObject["zIndex"] as? Int) ?? 0) | ||
| } | ||
| } |
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
884866
0.17%59
-1.67%2
100%