Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@capacitor/google-maps

Package Overview
Dependencies
Maintainers
17
Versions
443
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@capacitor/google-maps - npm Package Compare versions

Comparing version
7.1.1
to
7.2.0-spm-dev.1
+1281
ios/Sources/Capaci...sPlugin/CapacitorGoogleMapsPlugin.swift
// 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