This commit is contained in:
metacryst
2026-01-09 11:14:27 -06:00
parent cf03c95664
commit 637c9e4674
2149 changed files with 527743 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
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)
}
}

View File

@@ -0,0 +1,29 @@
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
}
}
}

View File

@@ -0,0 +1,157 @@
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)
}
}

View File

@@ -0,0 +1,50 @@
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)")
}
}

View File

@@ -0,0 +1,16 @@
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
}
}

View File

@@ -0,0 +1,807 @@
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?
var onViewDidLoad: (() -> Void)?
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
self.onViewDidLoad?()
}
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.mapViewController.onViewDidLoad = { [weak self] in
self?.finishMapConfiguration()
}
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)
}
}
}
func finishMapConfiguration() {
DispatchQueue.main.async {
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 actualHeightFloor = floor(height / 2)
let actualHeightCeil = ceil(height / 2)
let isWidthEqual = width == refWidth
let isHeightEqual = actualHeightFloor == refHeight || actualHeightCeil == refHeight
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
}
}

View File

@@ -0,0 +1,80 @@
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
}
}

View File

@@ -0,0 +1,84 @@
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
}
}

View File

@@ -0,0 +1,75 @@
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
}
}

View File

@@ -0,0 +1,18 @@
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)
}
}

View File

@@ -0,0 +1,25 @@
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)
}
}