diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/w3w-swift-components-map-apple.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/w3w-swift-components-map-apple.xcscheme
new file mode 100644
index 0000000..e283084
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/w3w-swift-components-map-apple.xcscheme
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Package.resolved b/Package.resolved
index d021fe7..f9a968c 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,13 +1,22 @@
{
- "originHash" : "2d2ff9066c4dc07bb0a0d8e58d69518e67d94b8d4e3545ed8609ccc5deddf020",
+ "originHash" : "99c32a712676667ed5ab12dc5bf2960f71e70611de03347238eda66e64f361ef",
"pins" : [
+ {
+ "identity" : "w3w-swift-app-events",
+ "kind" : "remoteSourceControl",
+ "location" : "git@github.com:w3w-internal/w3w-swift-app-events.git",
+ "state" : {
+ "revision" : "f9eb5e0ef166e6d056910b37e6516754a657470e",
+ "version" : "5.2.0"
+ }
+ },
{
"identity" : "w3w-swift-components-map",
"kind" : "remoteSourceControl",
"location" : "git@github.com:what3words/w3w-swift-components-map.git",
"state" : {
- "branch" : "main",
- "revision" : "c311d5682e05f87a910955a8acc78a2e1104ee67"
+ "revision" : "c26ebe3059f503c0545c7ad62b1573c79b61e8ce",
+ "version" : "1.0.0"
}
},
{
@@ -15,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/what3words/w3w-swift-core.git",
"state" : {
- "revision" : "598fc648928208d9f0268680ce15b686329d12f9",
- "version" : "1.1.2"
+ "revision" : "7fe8769bfbe105bcf3b05f42aeb8833673780ec1",
+ "version" : "1.1.4"
}
},
{
@@ -24,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/what3words/w3w-swift-design.git",
"state" : {
- "revision" : "ce6b9c951d1526915d484c7d723298a0ea3654ca",
- "version" : "1.0.8"
+ "revision" : "2495f4f80c1a872264211ac937f71d15d8516490",
+ "version" : "1.1.0"
}
},
{
@@ -33,8 +42,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/what3words/w3w-swift-themes.git",
"state" : {
- "revision" : "718555fc83d5745724a222c6ba39add74e0d345e",
- "version" : "1.2.2"
+ "revision" : "c6249273234f554a452e69ee06d2825d60c7c838",
+ "version" : "1.4.0"
}
}
],
diff --git a/Package.swift b/Package.swift
index 7ab0815..0b693ef 100644
--- a/Package.swift
+++ b/Package.swift
@@ -15,7 +15,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/what3words/w3w-swift-themes.git", "1.0.0"..<"2.0.0"),
.package(url: "https://github.com/what3words/w3w-swift-design.git", "1.0.0"..<"2.0.0"),
- .package(url: "git@github.com:what3words/w3w-swift-components-map.git", branch: "main"),
+ .package(url: "git@github.com:what3words/w3w-swift-components-map.git", "1.0.0"..<"2.0.0"),
.package(url: "https://github.com/what3words/w3w-swift-core.git", "1.0.0"..<"2.0.0")
],
@@ -28,7 +28,7 @@ let package = Package(
.product(name: "W3WSwiftCore", package: "w3w-swift-core"),
.product(name: "W3WSwiftDesign", package: "w3w-swift-design"),
.product(name: "W3WSwiftComponentsMap", package: "w3w-swift-components-map"),
- .product(name: "W3WSwiftThemes", package: "w3w-swift-themes")
+ .product(name: "W3WSwiftThemes", package: "w3w-swift-themes"),
]
),
diff --git a/Sources/W3WSwiftComponentsMapApple/Drawing/W3WAppleMapDrawer.swift b/Sources/W3WSwiftComponentsMapApple/Drawing/W3WAppleMapDrawer.swift
new file mode 100644
index 0000000..b2a884c
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Drawing/W3WAppleMapDrawer.swift
@@ -0,0 +1,1034 @@
+//
+// W3WApplemapViewProtocol.swift
+// w3w-swift-components-map-apple
+//
+// Created by Henry Ng on 17/1/25.
+//
+
+#if !os(macOS) && !os(watchOS)
+
+import Foundation
+import MapKit
+import W3WSwiftCore
+import W3WSwiftThemes
+import W3WSwiftComponentsMap
+import Combine
+
+public protocol W3WAppleMapDrawerProtocol {
+
+ var mapView: MKMapView? { get }
+ var region: MKCoordinateRegion { get }
+ var overlays: [MKOverlay] { get }
+ var annotations: [MKAnnotation] { get }
+ var mapGridData: W3WAppleMapGridData? { get set }
+
+ func addOverlay(_ overlay: MKOverlay)
+ func addOverlay(_ overlay: MKOverlay, _ color: W3WColor?)
+ func removeOverlay(_ overlay: MKOverlay)
+ func removeOverlays(_ overlays: [MKOverlay])
+ func addAnnotation(_ annotation: MKAnnotation)
+ func removeAnnotation(_ annotation: MKAnnotation)
+
+ func setRegion(_ region: MKCoordinateRegion, animated: Bool)
+ func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool)
+
+}
+
+extension W3WAppleMapDrawerProtocol {
+
+ public func updateMap() {
+
+ updateGrid()
+
+ if let lastZoomPointsPerSquare = mapGridData?.lastZoomPointsPerSquare {
+ let squareSize = getPointsPerSquare()
+ if (squareSize < CGFloat(12.0) && lastZoomPointsPerSquare > CGFloat(12.0)) || (squareSize > CGFloat(12.0) && lastZoomPointsPerSquare < CGFloat(12.0)) {
+
+ redrawPins()
+ }
+ mapGridData?.lastZoomPointsPerSquare = squareSize
+ }
+ }
+
+ func updateGrid() {
+ updateGridAlpha()
+ mapGridData?.gridUpdateDebouncer.closure = { _ in self.makeGrid() }
+ mapGridData?.gridUpdateDebouncer.execute(())
+
+ }
+
+ func updateGridAlpha() {
+
+ var alpha = CGFloat(0.0)
+
+ let pointsPerSquare = self.getPointsPerSquare()
+ if pointsPerSquare > mapGridData?.pointsPerSquare ?? CGFloat(12.0) {
+ alpha = (pointsPerSquare - (mapGridData?.pointsPerSquare ?? CGFloat(12.0)) ) / CGFloat(11.001)
+ }
+
+ if alpha > 1.0 {
+ alpha = 1.0
+
+ } else if alpha < 0.0 {
+ alpha = 0.0
+ }
+
+ mapGridData?.gridRenderer?.alpha = alpha
+ }
+
+ func makeGrid() {
+
+ let sw = CLLocationCoordinate2D(latitude: region.center.latitude - region.span.latitudeDelta * 3.0, longitude: region.center.longitude - region.span.longitudeDelta * 3.0)
+ let ne = CLLocationCoordinate2D(latitude: region.center.latitude + region.span.latitudeDelta * 3.0, longitude: region.center.longitude + region.span.longitudeDelta * 3.0)
+
+ // call w3w api for lines, if the area is not too great
+ if let distance = mapGridData?.w3w?.distance(from: sw, to: ne) {
+ if distance < W3WSettings.maxMetersDiagonalForGrid && distance > 0.0 {
+
+ mapGridData?.w3w?.gridSection(southWest:sw, northEast:ne) { lines, error in
+ self.makeNewGrid(lines: lines)
+ }
+ }
+ }
+ }
+
+ func makeNewGrid(lines: [W3WLine]?) {
+ DispatchQueue.main.async {
+ self.makePolygons(lines: lines)
+ // replace the overlay with a new one with the new lines
+ if let overlay = mapGridData?.gridLines {
+ self.removeGrid()
+ addOverlay(overlay)
+ }
+ }
+ }
+
+ func makePolygons(lines: [W3WLine]?) {
+
+ var multiLine = [MKPolyline]()
+
+ for line in lines ?? [] {
+ multiLine.append(MKPolyline(coordinates: [line.start, line.end], count: 2))
+ }
+ mapGridData?.gridLines = W3WMapGridLines(multiLine)
+ }
+
+ public func mapRenderer(overlay: MKOverlay) -> MKOverlayRenderer? {
+
+ if let o = overlay as? W3WMapGridLines {
+ return getMapGridRenderer(overlay: o)
+ }
+
+ if let o = overlay as? W3WMapSquareLines {
+ return getMapSquaresRenderer(overlay: o)
+ }
+
+ return MKOverlayRenderer()
+ }
+
+ func getMapGridRenderer(overlay: MKOverlay) -> MKOverlayRenderer? {
+
+ if let gridLines = overlay as? W3WMapGridLines {
+ mapGridData?.gridRenderer = W3WMapGridRenderer(multiPolyline: gridLines)
+ mapGridData?.gridRenderer?.strokeColor = mapGridData?.scheme?.colors?.line?.uiColor
+ mapGridData?.gridRenderer?.lineWidth = mapGridData?.mapGridLineThickness.value.value ?? CGFloat(0.5)
+ updateGridAlpha()
+ return mapGridData?.gridRenderer
+ }
+
+ return nil
+ }
+
+ func getMapSquaresRenderer(overlay: MKOverlay) -> MKOverlayRenderer? {
+
+ guard let mapGridData = self.mapGridData else { return nil }
+
+ if let square = overlay as? W3WMapSquareLines {
+ let squareRenderer = W3WMapSquaresRenderer(overlay: square)
+
+ let boxId = square.box.id
+
+ let isSelectedSquare = mapGridData.selectedSquare?.bounds?.id == boxId
+
+ let isMarker = mapGridData.markers.contains(where: { $0.bounds?.id == boxId })
+
+ let isSquare = mapGridData.squares.contains(where: { $0.bounds?.id == boxId })
+
+ var bgSquareColor: W3WColor?
+
+ if let color = mapGridData.overlayColors[boxId] {
+ bgSquareColor = color
+ }
+
+ let w3wImage: UIImage?
+ w3wImage = W3WImageCache.shared.getImage(for: bgSquareColor ?? .w3wBrandBase, size: CGSize(width: 40, height: 40)) ?? W3WImageCache.shared.getImage(for: .w3wBrandBase, size: CGSize(width: 40, height: 40))
+
+ squareRenderer.strokeColor = mapGridData.scheme?.colors?.line?.uiColor
+ squareRenderer.lineWidth = 0.1
+
+ if (isSelectedSquare) {
+ if (isMarker == true) {
+ squareRenderer.lineWidth = 1.0
+ if (isSquare == true) { // in list
+ squareRenderer.setSquareImage(w3wImage)
+ }
+ }
+ else {
+ squareRenderer.setSquareImage(w3wImage)
+ }
+
+ } else {
+
+ if (isMarker == true) {
+ squareRenderer.strokeColor = mapGridData.scheme?.colors?.line?.uiColor
+ squareRenderer.lineWidth = 1.0
+
+ if (bgSquareColor != nil) {
+ squareRenderer.setSquareImage(w3wImage)
+ }
+ }
+ else{
+ if (bgSquareColor != nil) {
+ squareRenderer.setSquareImage(w3wImage)
+ }
+ }
+ }
+
+ mapGridData.squareRenderer = squareRenderer
+ return mapGridData.squareRenderer
+
+ }
+ return nil
+ }
+
+ /// remove the grid overlay
+ func removeGrid() {
+ let gridOverlays = overlays.compactMap { $0 as? W3WMapGridLines }
+ if !gridOverlays.isEmpty {
+ removeOverlays(gridOverlays)
+ }
+ }
+
+ func getPointsPerSquare() -> CGFloat {
+ let threeMeterMapSquare = MKCoordinateRegion(center: mapView!.centerCoordinate, latitudinalMeters: 3, longitudinalMeters: 3);
+ let threeMeterViewSquare = mapView!.convert(threeMeterMapSquare, toRectTo: nil)
+
+ return threeMeterViewSquare.size.height
+ }
+
+ func redrawAll() {
+ redrawPins()
+ redrawGrid()
+ redrawSquares()
+ }
+
+ /// force a redrawing of all grid lines
+ func redrawGrid() {
+ makeGrid()
+ }
+
+ /// force a redrawing of all annotations
+ func redrawPins() {
+
+ for annotation in annotations {
+ removeAnnotation(annotation)
+ addAnnotation(annotation)
+ }
+ }
+
+ func redrawSquares() {
+ self.updateSquares()
+ }
+}
+
+extension W3WAppleMapDrawerProtocol {
+
+ // MARK: Pins / Annotations
+
+ func getMapAnnotationView(annotation: MKAnnotation, transitionScale: CGFloat) -> MKAnnotationView? {
+
+ if let a = annotation as? W3WAppleMapAnnotation {
+ let squareSize = getPointsPerSquare()
+ if squareSize > CGFloat(12.0) {
+ if let square = a.square {
+ showOutline(square, a)
+ }
+ //return an empty box
+ return MKAnnotationView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: 0.0))
+ } else {
+ if let square = a.square {
+ hideOutline(square)
+ }
+ return getMapPinView(annotation: a)
+ }
+ }
+ return nil
+ }
+
+ func showOutline(_ square: W3WSquare, _ annotation: W3WAppleMapAnnotation? = nil) {
+
+ W3WThread.runInBackground {
+
+ if let s = self.ensureSquareHasCoordinates(square: square),
+ let bounds = s.bounds,
+ let gridData = self.mapGridData {
+
+ W3WThread.runOnMain {
+
+ if (annotation?.isMarker == true ) {
+ if let m = annotation?.square {
+ gridData.markers.removeAll()
+ gridData.markers.append(m)
+ }
+ if (annotation?.isSaved == true) {
+ self.addUniqueSquare(s)
+ }
+
+ }
+ else{
+ self.addUniqueSquare(s)
+ }
+
+ //square is selected, without background only border
+ if let squareIsMarker = gridData.squareIsMarker {
+ self.addUniqueSquare(squareIsMarker)
+ }
+
+ let boundsId = bounds.id
+ gridData.overlayColors[boundsId] = annotation?.color
+ }
+
+ DispatchQueue.main.sync(flags: .barrier) { }
+ self.updateSquares()
+ self.updateSelectedSquare()
+ }
+ }
+ }
+
+ func updateSquares() {
+
+ guard let gridData = self.mapGridData else { return }
+
+ let group = DispatchGroup()
+
+ var squareId_color = [Int64: W3WColor]()
+
+ // Enter the group before starting work
+ group.enter()
+ // Get colors from main thread
+ W3WThread.runOnMain {
+ squareId_color = gridData.overlayColors
+ group.leave() // Signal that colors are ready
+ }
+ // Wait for colors to be copied
+ group.wait()
+
+ // Initialize the hash tracking variable if needed
+ if gridData.previousStateHash == nil {
+ gridData.previousStateHash = 0
+ }
+
+ // Create a comprehensive hash of the entire rendering state
+ var stateHasher = Hasher()
+
+ // Hash the square IDs
+ for square in gridData.squares {
+ if let id = square.bounds?.id {
+ stateHasher.combine(id)
+ }
+ }
+
+ // Hash the colors
+ for (id, color) in squareId_color {
+ stateHasher.combine(id)
+ stateHasher.combine(color.description)
+ }
+
+ // Hash the selected square
+ if let selectedId = gridData.selectedSquare?.bounds?.id {
+ stateHasher.combine(selectedId)
+ }
+
+ let currentStateHash = stateHasher.finalize()
+
+ // If nothing has changed, skip the update
+ if currentStateHash == gridData.previousStateHash && gridData.previousStateHash != 0 && gridData.squares.count != 0 {
+ // return
+ }
+
+ // Update hash for next comparison
+ gridData.previousStateHash = currentStateHash
+
+ var boxes = [(polyline: W3WMapSquareLines, color: W3WColor?)]()
+
+ for square in gridData.squares {
+ if let ne = square.bounds?.northEast,
+ let sw = square.bounds?.southWest {
+
+ let nw = CLLocationCoordinate2D(latitude: ne.latitude, longitude: sw.longitude)
+ let se = CLLocationCoordinate2D(latitude: sw.latitude, longitude: ne.longitude)
+ let polyline = W3WMapSquareLines(coordinates: [nw, ne, se, sw, nw], count: 5)
+
+ let boxId = square.bounds?.id ?? 0
+ let color = squareId_color[boxId]
+
+ boxes.append((polyline: polyline, color: color))
+ }
+ }
+
+
+ let iBoxes = boxes
+
+ W3WThread.runOnMain {
+
+ self.removeSquareOverlays()
+ for box in iBoxes {
+ addOverlay(box.polyline, box.color)
+ }
+ }
+ }
+
+ func hideOutline(_ square: W3WSquare) {
+ guard let gridData = self.mapGridData else { return }
+
+ W3WThread.runInBackground {
+ if let squares = self.mapGridData?.squares {
+ if !squares.isEmpty {
+ gridData.squares.removeAll(where: { s in
+ s.bounds?.id == square.bounds?.id
+ })
+ }
+ }
+ W3WThread.runOnMain {
+ self.removeColoredSquare(square)
+ self.updateSquares()
+ }
+
+ }
+ }
+
+ public func removeColoredSquare(_ square: W3WSquare) {
+
+ guard let gridData = self.mapGridData else { return }
+ if let id = square.bounds?.id {
+ gridData.overlayColors.removeValue(forKey: id)
+
+ }
+ }
+
+ public func removeColoredSquares() {
+
+ guard let gridData = self.mapGridData else { return }
+ if !gridData.overlayColors.isEmpty {
+ gridData.overlayColors = [:]
+ }
+ }
+
+ /// remove the grid overlay
+ func removeSquareOverlay(_ square: W3WSquare) {
+ // Remove them all in one batch operation
+ let squareOverlays = overlays.compactMap { $0 as? W3WMapSquareLines }
+
+ if let sOverlay = squareOverlays.first(where: { $0.box.id == square.bounds?.id }) {
+ removeOverlay(sOverlay)
+ }
+ }
+
+ /// remove the grid overlay
+ func removeSquareOverlays() {
+ for overlay in overlays {
+ if let squareOverlay = overlay as? W3WMapSquareLines {
+ removeOverlay(squareOverlay)
+ }
+ }
+ }
+
+ func removeSelectedSquare() {
+ guard let gridData = self.mapGridData else { return }
+
+ let markers = gridData.markers
+ if !markers.isEmpty {
+ gridData.markers.removeAll()
+ }
+
+ }
+
+ func updateSelectedSquare() {
+ guard let gridData = self.mapGridData else { return }
+
+ let markers = gridData.markers
+ self.makeMarkers(markers)
+ }
+
+ func makeMarkers(_ markers: [W3WSquare]?) {
+
+ var boxes = [MKPolyline]()
+
+ for (index, marker) in (markers ?? []).enumerated() {
+
+ if let ne1 = marker.bounds?.northEast,
+ let sw1 = marker.bounds?.southWest {
+
+ let nw1 = CLLocationCoordinate2D(latitude: ne1.latitude, longitude: sw1.longitude)
+ let se1 = CLLocationCoordinate2D(latitude: sw1.latitude, longitude: ne1.longitude)
+ boxes.append(W3WMapSquareLines(coordinates: [nw1, ne1, se1, sw1, nw1], count: 5))
+ }
+ }
+
+ W3WThread.runOnMain {
+ for bx in boxes {
+ addOverlay(bx)
+ }
+ }
+ }
+
+ func findSquare(_ square: W3WSquare) -> W3WSquare? {
+ for s in self.mapGridData?.squares ?? [] {
+ if s.bounds?.id == square.bounds?.id {
+ return s
+ }
+ }
+ return nil
+ }
+
+ func findMarker(_ square: W3WSquare) -> W3WSquare? {
+ for m in self.mapGridData?.markers ?? [] {
+ if m.bounds?.id == square.bounds?.id {
+ return m
+ }
+ }
+ return nil
+ }
+
+ func hideOutline(_ words: String) {
+ self.mapGridData?.squares.removeAll(where: { s in
+ return s.words == words
+ })
+ self.updateSquares()
+ }
+
+ func getMapPinView(annotation: W3WAppleMapAnnotation) -> MKAnnotationView? {
+ return pinView(annotation: annotation)
+ }
+
+ /// make a custom annotation view
+ func pinView(annotation: W3WAppleMapAnnotation) -> MKAnnotationView? {
+
+ let identifier = "w3wPin"
+ let color : W3WColor? = annotation.color
+ var pinImage: UIImage?
+ var centerOffset: CGPoint = .zero
+
+ let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
+
+ if case .circle = annotation.type {
+
+ let circleWidth = mapGridData?.pinWidth ?? CGFloat(30.0)
+ let circleHeight = mapGridData?.pinHeight ?? CGFloat(30.0)
+ let circleFrameSize = mapGridData?.pinFrameSize ?? CGFloat(30.0)
+
+ pinImage = W3WImage(drawing: .mapCircle, colors: .standardMaps.with(background: color).with(foreground: color?.complimentaryTextColor())).get(size: W3WIconSize(value: CGSize(width: circleWidth , height: circleHeight)))
+
+ centerOffset = CGPoint(x: 0.0, y: 0.0)
+ annotationView.image = pinImage
+ annotationView.frame.size = CGSize(width: circleFrameSize, height: circleFrameSize)
+
+ if #available(iOS 14.0, *) {
+ annotationView.zPriority = .defaultUnselected
+ }
+ }
+
+ if case .square = annotation.type {
+
+ let squareSize = mapGridData?.pinSquareSize ?? CGFloat(50.0)
+ let pinImageWidth = squareSize
+ let pinImageHeight = squareSize
+
+ pinImage = W3WImage(drawing: .mapPin, colors: .standard.with(background: color).with(foreground: color?.complimentaryTextColor()))
+ .get(size: W3WIconSize(value: CGSize(width: pinImageWidth , height: pinImageHeight)))
+
+ centerOffset = CGPoint(x: 0.0, y: (-20.0))
+ annotationView.image = pinImage
+ annotationView.frame.size = CGSize(width: squareSize, height: squareSize)
+
+ if #available(iOS 14.0, *) {
+ annotationView.zPriority = .max
+ }
+
+ }
+ annotationView.centerOffset = centerOffset
+ // Make the pin selectable
+ annotationView.canShowCallout = true
+ annotationView.isEnabled = true
+ return annotationView
+ }
+}
+
+extension W3WAppleMapDrawerProtocol {
+
+ // MARK: SQUARES CHECK
+
+ func ensureSquareHasCoordinates(square: W3WSquare) -> W3WSquare? {
+ let s = ensureSquaresHaveCoordinates(squares: [square])
+ return s.first
+ }
+
+ func ensureSquaresHaveCoordinates(squares: [W3WSquare]) -> [W3WSquare] {
+ //checkConfiguration()
+ if W3WThread.isMain() {
+ print(#function, " must NOT be called on main thread")
+ abort()
+ }
+
+ var goodSquares = [W3WSquare]()
+
+ let tasks = DispatchGroup()
+
+ // for each square, make sure it is complete with coordinates and words
+ for square in squares {
+ tasks.enter()
+ complete(square: square) { completeSquare in
+ if let s = completeSquare {
+ goodSquares.append(s)
+ }
+ tasks.leave()
+ }
+ }
+
+ // wait for all the squares to be completed
+ tasks.wait()
+
+ return goodSquares
+ }
+
+ func completeSquareWithCoordinates(square: W3WSquare) -> W3WSquare? {
+ let completedSquares = completeSquaresWithCoordinates(squares: [square])
+ return completedSquares.first
+ }
+
+ func convertToSquaresWithCoordinates(words: [String]) -> [W3WSquare] {
+ var squares = [W3WSquare]()
+
+ for word in words {
+ squares.append(W3WBaseSquare(words: word))
+ }
+
+ return ensureSquaresHaveCoordinates(squares: squares)
+ }
+
+ func convertToSquaresWithCoordinates(suggestions: [W3WSuggestion]) -> [W3WSquare] {
+ var squares = [W3WSquare]()
+
+ for suggestion in suggestions {
+ squares.append(W3WBaseSquare(words: suggestion.words))
+ }
+
+ return ensureSquaresHaveCoordinates(squares: squares)
+ }
+
+ func convertToSquares(coordinates: [CLLocationCoordinate2D]) -> [W3WSquare] {
+ var squares = [W3WSquare]()
+
+ for coordinate in coordinates {
+ squares.append(W3WBaseSquare(coordinates: coordinate))
+ }
+
+ return ensureSquaresHaveCoordinates(squares: squares)
+ }
+
+ func completeSquaresWithCoordinates(squares: [W3WSquare]) -> [W3WSquare] {
+
+ if W3WThread.isMain() {
+
+ let error = W3WError.message("must NOT be called on main thread")
+ // self.errorHandler(error: error)
+ print(#function, " must NOT be called on main thread")
+ abort()
+ }
+
+ var completedSquares = [W3WSquare]()
+ let tasks = DispatchGroup()
+
+ // for each square, make sure it is complete with coordinates and words
+ for square in squares {
+ tasks.enter()
+ complete(square: square) { completeSquare in
+ if let s = completeSquare {
+ completedSquares.append(s)
+ }
+ tasks.leave()
+ }
+ }
+
+ // wait for all the squares to be completed
+ tasks.wait()
+
+ return completedSquares
+ }
+
+ /// check a square and fill out it's words or coordinates as needed, then return a completed square via completion block
+ func complete(square: W3WSquare, completion: @escaping (W3WSquare?) -> ()) {
+
+ // if the square has words but no coordinates
+ if square.coordinates == nil {
+ if let words = square.words {
+ self.mapGridData?.w3w?.convertToCoordinates(words: words) { result, error in
+ // self.errorHandler(error: error)
+ completion(result)
+ }
+
+ // else if the square has no words and no coordinates then it is useless and we omit it
+ } else {
+ completion(nil)
+ }
+
+ // else if the square has coordinates but no words
+ } else if square.words == nil {
+ if let coordinates = square.coordinates {
+ self.mapGridData?.w3w?.convertTo3wa(coordinates: coordinates, language: self.mapGridData?.language ?? W3WSettings.defaultLanguage ) { result, error in
+ // self.errorHandler(error: error)
+ completion(result)
+ }
+
+ // else if the square has no words and no coordinates then it is useless and we omit it
+ } else {
+ completion(nil)
+ }
+
+ // else the square already has coordinates and words
+ } else {
+ completion(square)
+ }
+ }
+
+ /// put a what3words annotation on the map showing the address
+ func addMarker(at words: String?, color: W3WColor? = nil, type: W3WMarkerType, completion: @escaping MarkerCompletion = { _,_ in }) {
+ W3WThread.runOnMain {
+ if let w = words {
+ self.mapGridData?.w3w?.convertToCoordinates(words: w) { square, error in
+ // self.errorHandler(error: error)
+ if let s = square {
+ self.addMarker(at: s, color: color, type: type)
+ }
+ }
+ }
+ }
+ }
+
+ /// put a what3words annotation on the map showing the address
+ public func addMarker(at words: [String]?, color: W3WColor? = nil, type: W3WMarkerType, completion: @escaping MarkerCompletion = { _,_ in }) {
+ W3WThread.runOnMain {
+ if let w = words {
+ W3WThread.runInBackground {
+ let squaresWithCoords = self.convertToSquaresWithCoordinates(words: w)
+
+ // if there's a bad square then error out
+ if squaresWithCoords.count != words?.count {
+ completion(nil, W3WError.message("Invalid three word address, or coordinates passed into map function"))
+ // otherwise go for it
+ } else {
+ self.addMarker(at: squaresWithCoords, color: color, type: type)
+ }
+ }
+ }
+ }
+ }
+
+ /// put a what3words annotation on the map showing the address, and optionally center the map around it
+ func addMarker(at square: W3WSquare?, color: W3WColor? = nil, type: W3WMarkerType, completion: @escaping MarkerCompletion = { _,_ in }) {
+ W3WThread.runInBackground {
+ if let sq = square {
+ if let s = self.completeSquareWithCoordinates(square: sq) {
+ W3WThread.runOnMain {
+ self.addAnnotation(square: s, color: color, type: type)
+ completion(s , nil)
+ }
+ }
+ }
+ }
+ }
+
+ /// add an annotation to the map given a square this compensates for missing words or missing
+ /// coordiantes, and does nothing if neither is present
+ /// this is the one that actually does the work. The other addAnnotations calls end up calling this one.
+ func addAnnotation(square: W3WSquare, color: W3WColor? = nil, type: W3WMarkerType, isMarker: Bool? = false, isMark: Bool? = false, isSaved: Bool? = false) {
+ W3WThread.runInBackground {
+ W3WThread.runOnMain {
+ self.removeMarker(at: square)
+ addAnnotation(W3WAppleMapAnnotation(square: square, color: color, type: type, isMarker: isMarker, isMark: isMark, isSaved: isSaved))
+ }
+ }
+ }
+
+ ///
+ func addSelectedMarker(at square: W3WSquare?, color: W3WColor? = nil, type: W3WMarkerType, isMarker: Bool? = false, isMark: Bool? = false, isSaved: Bool? = false, completion: @escaping MarkerCompletion = { _,_ in }) {
+ W3WThread.runOnMain {
+ if let sq = square {
+ W3WThread.runInBackground {
+ if let s = self.completeSquareWithCoordinates(square: sq) {
+ self.addAnnotation(square: s, color: color, type: type, isMarker: isMarker, isMark: isMark, isSaved: isSaved)
+ completion(s , nil)
+ }
+ }
+ }
+ }
+ }
+
+ func addCirclePin(square: W3WSquare, color: W3WColor? = nil) {
+ W3WThread.runOnMain {
+ // W3WThread.runInBackground {
+ addAnnotation(W3WAppleMapAnnotation(square: square, color: color, type: .circle, isMarker: false, isMark: false ))
+ // }
+ }
+ }
+
+ func addMarkerAsCircle(at square: W3WSquare?, color: W3WColor? = nil, completion: @escaping MarkerCompletion = { _,_ in }) {
+ W3WThread.runOnMain {
+ if let s = square {
+ self.addCirclePin(square: s, color: color)
+ completion(s , nil)
+ }
+ }
+ }
+
+ /// put a what3words annotation on the map showing the address
+ func addMarker(at suggestion: W3WSuggestion?, color: W3WColor? = nil, type: W3WMarkerType, completion: @escaping MarkerCompletion = { _,_ in }) {
+ if let words = suggestion?.words {
+ addMarker(at: words, color: color, type: type, completion: completion)
+ }
+ }
+
+ /// put a what3words annotation on the map showing the address
+ public func addMarker(at suggestions: [W3WSuggestion]?, color: W3WColor? = nil, type: W3WMarkerType, completion: @escaping MarkerCompletion = { _,_ in }) {
+ W3WThread.runOnMain {
+ if let s = suggestions {
+ W3WThread.runInBackground {
+ let squaresWithCoords = self.convertToSquaresWithCoordinates(suggestions: s)
+
+ // if there's a bad square then error out
+ if squaresWithCoords.count != suggestions?.count {
+ completion(nil, W3WError.message("Invalid three word address, or coordinates passed into map function"))
+
+ // otherwise go for it
+ } else {
+ self.addMarker(at: squaresWithCoords, color: color, type: type, completion: completion)
+ }
+ }
+ }
+ }
+ }
+
+ /// put a what3words annotation on the map showing the address
+ func addMarker(at coordinates: CLLocationCoordinate2D?, color: W3WColor? = nil, type: W3WMarkerType, completion: @escaping MarkerCompletion = { _,_ in }) {
+ W3WThread.runOnMain {
+ if let c = coordinates {
+ // self.checkConfiguration()
+ self.mapGridData?.w3w?.convertTo3wa(coordinates: c, language: self.mapGridData?.language ?? W3WSettings.defaultLanguage) { square, error in
+ // self.dealWithAnyApiError(error: error)
+ if let s = square {
+ self.addMarker(at: s, color: color, type: type, completion: completion)
+ }
+ }
+ }
+ }
+ }
+
+ /// put a what3words annotation on the map showing the address
+ public func addMarker(at coordinates: [CLLocationCoordinate2D]?, color: W3WColor? = nil, type: W3WMarkerType, completion: @escaping MarkerCompletion = { _,_ in }) {
+ W3WThread.runOnMain {
+ if let c = coordinates {
+ W3WThread.runInBackground {
+ let squaresWithCoords = self.convertToSquares(coordinates: c)
+
+ // if there's a bad square then error out
+ if squaresWithCoords.count != coordinates?.count {
+ completion(nil, W3WError.message("Invalid three word address, or coordinates passed into map function"))
+
+ // otherwise go for it
+ } else {
+ self.addMarker(at: squaresWithCoords, color: color, type: type, completion: completion)
+ }
+ }
+ }
+ }
+ }
+
+
+
+ /// put a what3words annotation on the map showing the address
+ func addMarker(at squares: [W3WSquare]?, color: W3WColor? = nil, type: W3WMarkerType, completion: @escaping MarkerCompletion = { _,_ in }) {
+
+ W3WThread.runOnMain {
+ if let s = squares {
+ W3WThread.runInBackground {
+ let goodSquares = self.ensureSquaresHaveCoordinates(squares: s)
+
+ // error out if not all squares are valid
+ if goodSquares.count != s.count {
+ completion(nil, W3WError.message("Invalid three word address, or coordinates passed into map function"))
+
+ // good squares, proceed to place on map
+ } else {
+ completion(goodSquares as! W3WSquare, nil)
+ }
+ }
+ }
+ }
+ }
+}
+
+extension W3WAppleMapDrawerProtocol {
+
+ /// remove a what3words annotation from the map if it is present
+ /// this is the one that actually does the work. The other remove calls
+ /// end up calling this one.
+
+ public func removeMarker(at square: W3WSquare?) {
+ if let s = square {
+ if let annotation = findAnnotation(s) {
+ removeAnnotation(annotation)
+ removeColoredSquare(s)
+ hideOutline(s)
+ }
+ }
+ }
+
+ /// remove a what3words annotation from the map if it is present
+ public func removeMarker(at suggestion: W3WSuggestion?) {
+ if let words = suggestion?.words {
+ removeMarker(at: words)
+ }
+ }
+
+ public func removeMarker(at words: String?) {
+ if let w = words {
+ for annotation in annotations {
+ if let a = annotation as? W3WAppleMapAnnotation {
+ if let square = a.square {
+ if square.words == w {
+ self.removeAnnotation(a)
+ self.hideOutline(w)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /// remove what3words annotations from the map if they are present
+ public func removeMarker(at suggestions: [W3WSuggestion]?) {
+ for suggestion in suggestions ?? [] {
+ removeMarker(at: suggestion)
+ }
+ }
+
+ /// remove what3words annotations from the map if they are present
+ public func removeMarker(at squares: [W3WSquare]?) {
+ for square in squares ?? [] {
+ removeMarker(at: square)
+ }
+ }
+
+ /// remove what3words annotations from the map if they are present
+ public func removeMarker(at words: [String]?) {
+ for word in words ?? [] {
+ removeMarker(at: word)
+ }
+ }
+
+
+ func hideOutlineMarker(_ square: W3WSquare) {
+
+ W3WThread.runInBackground {
+ if let s = self.mapGridData?.markers {
+ if s != nil {
+ self.mapGridData?.markers.removeAll(where: { m in
+ return m.bounds?.id == square.bounds?.id
+ })
+ }
+
+ }
+ self.updateSelectedSquare()
+ }
+
+ }
+
+ public func removeSelectedSquare(at square: W3WSquare?) {
+ if let s = square {
+ if let annotation = findAnnotation(s) {
+ removeAnnotation(annotation)
+ hideOutlineMarker(s)
+ }
+ }
+ }
+
+
+ func findAnnotation(_ square: W3WSquare?) -> W3WAppleMapAnnotation? {
+ for annotation in annotations {
+ if let a = annotation as? W3WAppleMapAnnotation {
+ if (a.square?.bounds?.id == square?.bounds?.id) {
+ return a
+ }
+ }
+ }
+ return nil
+ }
+ public func getAllMarkers() -> [W3WSquare] {
+ var squares = [W3WSquare]()
+
+ for annotation in annotations {
+ if let a = annotation as? W3WAppleMapAnnotation {
+ if let square = a.square {
+ squares.append(square)
+ }
+ }
+ }
+
+ return squares
+ }
+
+}
+
+extension W3WAppleMapDrawerProtocol {
+
+ /// set the map center to a coordinate, and set the minimum visible area
+ func set(center: CLLocationCoordinate2D) {
+ W3WThread.runOnMain {
+ self.setCenter(center, animated: true)
+ }
+ }
+
+
+ /// set the map center to a coordinate, and set the minimum visible area
+ func set(center: CLLocationCoordinate2D, latitudeSpanDegrees: Double, longitudeSpanDegrees: Double) {
+ W3WThread.runOnMain {
+ let coordinateRegion = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: latitudeSpanDegrees, longitudeDelta: longitudeSpanDegrees))
+ self.setRegion(coordinateRegion, animated: true)
+ }
+ }
+
+
+ /// set the map center to a coordinate, and set the minimum visible area
+ func set(center: CLLocationCoordinate2D, latitudeSpanMeters: Double, longitudeSpanMeters: Double) {
+ W3WThread.runOnMain {
+ let coordinateRegion = MKCoordinateRegion(center: center, latitudinalMeters: latitudeSpanMeters, longitudinalMeters: longitudeSpanMeters)
+ self.setRegion(coordinateRegion, animated: true)
+ }
+ }
+
+ func addUniqueSquare(_ square: any W3WSquare) {
+
+ if let gridData = self.mapGridData {
+
+ let exists = gridData.squares.contains { existingSquare in
+ return existingSquare.bounds?.id == square.bounds?.id
+ }
+ if !exists {
+ gridData.squares.append(square)
+ }
+ }
+ }
+}
+
+
+#endif
diff --git a/Sources/W3WSwiftComponentsMapApple/Helper/W3WAppleMapHelper.swift b/Sources/W3WSwiftComponentsMapApple/Helper/W3WAppleMapHelper.swift
new file mode 100644
index 0000000..d024015
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Helper/W3WAppleMapHelper.swift
@@ -0,0 +1,646 @@
+//
+// W3WAppleMapHelper.swift
+// w3w-swift-components-map-apple
+//
+// Created by Henry Ng on 16/12/24.
+//
+
+import MapKit
+import Foundation
+import W3WSwiftThemes
+import W3WSwiftCore
+import W3WSwiftComponentsMap
+import W3WSwiftDesign
+
+public class W3WAppleMapHelper: NSObject, W3WAppleMapDrawerProtocol, W3WAppleMapHelperProtocol {
+
+ public weak var mapView: MKMapView?
+
+ public var mapGridData: W3WAppleMapGridData?
+
+ public var scheme: W3WScheme? = W3WTheme.what3words.mapScheme()
+
+ public var region: MKCoordinateRegion {
+ return mapView?.region ?? MKCoordinateRegion()
+ }
+
+ public var annotations: [MKAnnotation] {
+ return mapView?.annotations ?? [MKAnnotation]()
+ }
+
+ public var overlays: [MKOverlay] {
+ get {
+ return mapView?.overlays ?? [MKOverlay]()
+ }
+ }
+
+ public var mapType: MKMapType {
+ get {
+ return mapView?.mapType as! MKMapType
+ }
+ set {
+ mapView?.mapType = newValue
+ self.redrawAll()
+ setGridColor()
+ }
+ }
+
+ public var language: W3WLanguage = W3WSettings.defaultLanguage
+
+
+ /// called when the user taps a square in the map
+ public var onSquareSelected: (W3WSquare) -> () = { _ in }
+
+ /// called when the user taps a square that has a marker added to it
+ public var onMarkerSelected: (W3WSquare) -> () = { _ in }
+
+ private var w3w: W3WProtocolV4
+
+ public private(set) var markers: [W3WSquare] = []
+
+ public init(mapView: MKMapView, _ w3w: W3WProtocolV4, language: W3WLanguage = W3WSettings.defaultLanguage ) {
+ self.mapView = mapView
+ self.w3w = w3w
+ super.init()
+
+ self.mapGridData = W3WAppleMapGridData(w3w: w3w, scheme: scheme, language: language)
+ self.language = language
+
+ }
+
+ func setGridColor() {
+ if let gridData = mapGridData {
+ gridData.mapGridColor.send(mapType == .standard ? .mediumGrey : .white)
+ }
+ }
+
+
+ func setGridLineThickness(value: W3WLineThickness) {
+ if let gridData = mapGridData {
+ gridData.mapGridLineThickness.send(value)
+ }
+ }
+
+ func setSquareLineThickness(value: W3WLineThickness) {
+ if let gridData = mapGridData {
+ gridData.mapSquareLineThickness.send(value)
+ }
+ }
+
+ func configure () {
+ self.mapView?.showsUserLocation = true
+ }
+
+ public func set(language: W3WLanguage) {
+ self.language = language
+ }
+
+ public func set(type: String) {
+ switch type {
+ case "standard": self.mapType = .standard
+ case "hybrid": self.mapType = .hybrid
+ case "satellite": self.mapType = .satellite
+
+ default: self.mapType = .standard
+ }
+ }
+
+ public func set(scheme: W3WScheme?) {
+ self.mapGridData?.set(scheme: scheme)
+ }
+
+ public func getType() -> W3WMapType {
+ switch self.mapType {
+ case .standard: return "standard"
+ case .satellite: return "satellite"
+ case .hybrid: return "hybridFlyover"
+
+ default: return "hybridFlyover"
+ }
+ }
+
+ public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
+ updateMap()
+ // changeLineThicknessIfNeeded()
+ }
+
+ /// hijack this delegate call and update the grid, then pass control to the external delegate
+ public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
+ updateMap()
+ }
+
+ /// hijack this delegate call and update the grid, then pass control to the external delegate
+ public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
+ updateMap()
+ }
+
+ public func mapView(_ mapView: MKMapView, mapTypeChanged type: MKMapType) {
+ updateMap()
+ }
+
+ public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation, with transitionScale: CGFloat) -> MKAnnotationView? {
+ if let a = getMapAnnotationView(annotation: annotation, transitionScale: transitionScale) {
+ return a
+ }
+
+ return nil
+ }
+
+ public func addAnnotation(_ annotation: MKAnnotation) {
+ mapView?.addAnnotation(annotation)
+ }
+
+ public func removeAnnotation(_ annotation: MKAnnotation) {
+ mapView?.removeAnnotation(annotation)
+
+ }
+
+ public func removeOverlay(_ overlay: MKOverlay) {
+ mapView?.removeOverlay(overlay)
+ }
+
+ public func removeOverlays(_ overlays: [MKOverlay]) {
+ mapView?.removeOverlays(overlays)
+ }
+
+ public func addOverlay(_ overlay: MKOverlay) {
+ mapView?.addOverlay(overlay)
+ }
+
+ public func addOverlays(_ overlays: [MKOverlay]) {
+ mapView?.addOverlays(overlays)
+ }
+
+ public func addOverlays(_ overlays: [MKOverlay], _ color: W3WColor?) {
+ mapView?.addOverlays(overlays)
+ }
+
+ public func addOverlay(_ overlay: MKOverlay, _ color: W3WColor? = nil) {
+
+ if let color = color, let square = overlay as? W3WMapSquareLines {
+ mapGridData?.overlayColors[square.box.id] = color
+ }
+ mapView?.addOverlay(overlay)
+
+ }
+
+ public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
+ if let markerView = view.annotation as? W3WAppleMapAnnotation {
+ }
+ }
+
+ public func mapView(_ mapView: MKMapView, didAdd renderers: [MKOverlayRenderer]) {
+
+ }
+
+ public func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
+ // Maintain a dictionary of positions by section
+ var positionsBySection = [String: [CGPoint]]()
+
+ // Minimum distance between pins in points
+ let minDistance: CGFloat = 5.0
+
+ for view in views {
+ guard let annotation = view.annotation as? W3WAppleMapAnnotation else { continue }
+
+ // Create a section key based on annotation's location
+ // Round to nearest grid to group nearby pins
+ let gridSize: Double = 0.0001 // Adjust based on your needs
+ let latValue: Double = annotation.square?.coordinates?.latitude ?? annotation.coordinate.latitude
+ let lngValue: Double = annotation.square?.coordinates?.longitude ?? annotation.coordinate.longitude
+
+ let latSection = Int(latValue / gridSize)
+ let lngSection = Int(lngValue / gridSize)
+ let sectionKey = "\(latSection)_\(lngSection)"
+
+ // Get current center point for this view
+ let center = view.center
+
+ // Get existing positions in this section
+ var positions = positionsBySection[sectionKey] ?? []
+
+ // Check if this position is too close to existing ones
+ var needsAdjustment = false
+ for existingPos in positions {
+ let distance = hypot(center.x - existingPos.x, center.y - existingPos.y)
+ if distance < minDistance {
+ needsAdjustment = true
+ break
+ }
+ }
+
+ // If too close, apply a small offset
+ if needsAdjustment {
+ // Calculate offset direction (try to avoid overlaps)
+ let offsetX = CGFloat(arc4random_uniform(2) == 0 ? -1 : 1) * minDistance * 0.7
+ let offsetY = CGFloat(arc4random_uniform(2) == 0 ? -1 : 1) * minDistance * 0.7
+
+ // Apply offset
+ view.centerOffset = CGPoint(
+ x: view.centerOffset.x + offsetX,
+ y: view.centerOffset.y + offsetY
+ )
+ }
+
+ // Add this position to our tracking dictionary
+ positions.append(view.center)
+ positionsBySection[sectionKey] = positions
+ }
+ }
+
+}
+
+public extension W3WAppleMapHelper {
+
+ func select(at coordinates: CLLocationCoordinate2D, completion: @escaping (Result) -> Void) {
+
+ self.convertTo3wa(coordinates: coordinates, language: self.language) { [weak self] square, error in
+
+ guard let self = self else { return }
+
+ if let e = error {
+ W3WThread.runOnMain {
+ self.mapGridData?.onError(e)
+ completion(.failure(e))
+ }
+ }
+ if let s = square {
+ W3WThread.runOnMain {
+ // self.select(at: s)
+ completion(.success(s))
+ }
+ } else {
+ W3WThread.runOnMain {
+ let e = W3WError.message("No Square Found")
+ completion(.failure(e))
+ }
+ }
+ }
+ }
+
+ func select(at: W3WSquare) {
+ createMarkerForConditions(at)
+ }
+
+ func createMarkerForConditions(_ at: W3WSquare) {
+
+ let squares = self.mapGridData?.squares
+
+ let selectedSquare = self.mapGridData?.selectedSquare
+
+ let isMarkerinList = squares?.contains(where: { $0.bounds?.id == at.bounds?.id })
+
+ let isPrevMarkerinList = squares?.contains(where: { $0.bounds?.id == selectedSquare?.bounds?.id })
+
+ let annotation = findAnnotation(selectedSquare)
+
+ let markers = self.mapGridData?.markers
+
+ let squareSize = getPointsPerSquare()
+
+ if let selectedSquare = selectedSquare {
+ if squareSize < self.mapGridData?.pointsPerSquare ?? CGFloat(12.0) {
+ if (annotation?.isMarker == true && annotation?.isMark == false ) { //check the previous annotation is square
+ let previousBoxId = selectedSquare.bounds?.id
+
+ if let previousColor = self.mapGridData?.overlayColors[previousBoxId ?? 0] {
+ removeSelectedSquare(at: selectedSquare)
+ addMarkerAsCircle(at: selectedSquare, color: previousColor)
+ }
+ else{
+ if markers != nil {
+ removeSelectedSquare(at: selectedSquare)
+ addMarkerAsCircle(at: selectedSquare, color: annotation?.color)
+ }
+ }
+ }
+ else{
+ removeSelectedSquare(at: selectedSquare)
+ }
+ let color: W3WColor = self.mapGridData?.scheme?.colors?.border ?? .black
+ addSelectedMarker(at: at, color: color, type: .square, isMarker: true, isMark: true)
+ self.mapGridData?.selectedSquare = at
+
+ return
+ }
+ removeSelectedSquare(at: selectedSquare)
+ }
+
+ //squares
+ if isMarkerinList == true {
+ removeSelectedSquare(at: selectedSquare)
+
+ let currentBoxId = at.bounds?.id
+ let previousBoxId = selectedSquare?.bounds?.id
+ if isPrevMarkerinList == true {
+ if let previousColor = self.mapGridData?.overlayColors[previousBoxId ?? 0] {
+ addMarker(at: selectedSquare, color: previousColor, type: .circle)
+ }
+ }
+ if let color = self.mapGridData?.overlayColors[currentBoxId ?? 0] {
+ addSelectedMarker(at: at, color: color, type: .square, isMarker: true, isMark: false)
+ }
+ self.mapGridData?.squareIsMarker = at
+
+ } else {
+ let previousBoxId = selectedSquare?.bounds?.id
+ if isPrevMarkerinList == true {
+ if let previousColor = self.mapGridData?.overlayColors[previousBoxId ?? 0] {
+ addMarkerAsCircle(at: selectedSquare, color: previousColor)
+ }
+ }
+
+ let color: W3WColor = self.mapGridData?.scheme?.colors?.border ?? .black
+ addSelectedMarker(at: at, color: color, type: .square, isMarker: true, isMark: true)
+ }
+
+ self.mapGridData?.selectedSquare = at
+ }
+
+ /// put a what3words annotation on the map showing the address
+ func addMarker(at square: W3WSquare?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion) {
+ addMarker(at: square, color: color, type: type, completion: completion)
+ }
+
+ func addMarker(at suggestion: W3WSuggestion?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion) {
+ addMarker(at: suggestion, color: color, type: type, completion: completion)
+ }
+
+ func addMarker(at word: String?, color: W3WSwiftThemes.W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion) {
+
+ addMarker(at: word, color: color, type: type, completion: completion)
+ }
+
+ func addMarker(at words: [String]?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion) {
+ addMarker(at: words, color: color, type: type, completion: completion)
+ }
+
+ func addMarker(at coordinate: CLLocationCoordinate2D?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion) {
+ addMarker(at: coordinate, color: color, type: type, completion: completion)
+ }
+
+ func addMarker(at squares: [W3WSquare]?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion) {
+ addMarker(at: squares, color: color, type: type, completion: completion)
+ }
+
+ func addMarker(at suggestions: [W3WSuggestion]?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion) {
+ addMarker(at: suggestions, color: color, type: type, completion: completion)
+ }
+
+ func addMarker(at coordinates: [CLLocationCoordinate2D]?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion) {
+ addMarker(at: coordinates, color: color, type: type, completion: completion)
+ }
+
+ func removeMarker(at suggestion: W3WSuggestion?) {
+
+ }
+
+ func removeMarker(at words: String?) {
+
+ }
+
+ func removeMarker(at squares: [W3WSquare]?) {
+
+ }
+
+ func removeMarker(at suggestions: [W3WSuggestion]?) {
+
+ }
+
+ func removeMarker(at words: [String]?) {
+
+ }
+
+ func removeMarker(at square: W3WSquare?) {
+ // removeMarker(at: square)
+ }
+
+ func removeMarker(group: String) {
+
+ }
+
+ func unselect() {
+
+ }
+
+ func hover(at: CLLocationCoordinate2D) {
+
+ }
+
+ func unhover() {
+
+ }
+
+ func set(zoomInPointsPerSquare: CGFloat) {
+
+ }
+
+ func getAllMarkers() -> [W3WSquare] {
+ return [W3WSquare]()
+ }
+
+ func removeAllMarkers() {
+ self.markers.removeAll()
+
+ if let gridData = self.mapGridData {
+ gridData.squares.removeAll()
+ gridData.markers.removeAll()
+ // gridData.selectedSquare = nil
+ // gridData.squareIsMarker = nil
+ // gridData.currentSquare = nil
+ gridData.overlayColors = [:]
+ gridData.previousStateHash = nil
+
+ for annotation in annotations {
+ removeAnnotation(annotation)
+ }
+ }
+ }
+
+ func redraw(){
+ redrawGrid()
+ redrawSquares()
+ redrawPins()
+ }
+
+ func findMarker(by coordinates: CLLocationCoordinate2D) -> W3WSquare? {
+ return nil
+ }
+}
+
+extension W3WAppleMapHelper {
+
+ public func updateCamera(camera: W3WMapCamera?) {
+ W3WThread.runOnMain { [weak self] in
+ guard let self,
+ let mapView else { return }
+
+ let center = camera?.center ?? mapView.centerCoordinate
+ let scale = camera?.scale
+ let span = scale?.asSpan(mapSize: mapView.frame.size,
+ latitude: center.latitude)
+
+ // Check center validity
+ guard CLLocationCoordinate2DIsValid(center) else {
+ return
+ }
+
+ // Center and span are available -> make a region
+ if let span, span.latitudeDelta.isFinite, span.longitudeDelta.isFinite {
+ let region = MKCoordinateRegion(center: center, span: span)
+ mapView.setRegion(region, animated: true)
+
+ // No span -> no region -> move to center as a fallback option
+ } else if camera?.center != nil {
+ mapView.setCenter(center, animated: true)
+
+ } else {
+ // Do nothing
+ }
+ }
+ }
+
+ public func updateSquare(square: W3WSquare?) {
+ if let square = square {
+ self.mapGridData?.currentSquare = square
+ self.select(at: square)
+ }
+ }
+
+ private func getNewMarkers(markersLists: W3WMarkersLists) -> W3WMarkersLists {
+ guard let gridData = self.mapGridData else {
+ return W3WMarkersLists()
+ }
+
+ // Create a new markers list to return
+ let newMarkersLists = W3WMarkersLists()
+
+ // Clear the automatically created default list
+ newMarkersLists.lists.removeAll()
+
+ // Process each list in the input
+ for (listName, list) in markersLists.getLists() {
+ // Skip empty lists or default list with no color
+ if list.markers.isEmpty || (listName == "default" && list.color == nil) {
+ continue
+ }
+
+ // Create a new list for this color
+ let newList = W3WMarkerList()
+ newList.color = list.color
+ newList.type = list.type
+
+ // Track already processed squares for this specific list
+ var listProcessedIds = Set()
+
+ for marker in list.markers {
+ if let bounds = marker.bounds {
+ let squareId = bounds.id
+
+ // Skip if we've already processed this ID in this list
+ if listProcessedIds.contains(squareId) {
+ continue
+ }
+
+ // Check if this square exists in overlay colors
+ if let existingColor = gridData.overlayColors[squareId] {
+ // Only include if the color is different
+ if let listColor = list.color, !colorComponentsMatch(existingColor.cgColor, listColor.cgColor) {
+ newList.markers.append(marker)
+ }
+ } else {
+ // Square doesn't exist in overlay colors, so include it
+ newList.markers.append(marker)
+ }
+
+ // Mark this ID as processed for this list
+ listProcessedIds.insert(squareId)
+ } else {
+ // No bounds, include it
+ newList.markers.append(marker)
+ }
+ }
+
+ // Only add lists with markers
+ if !newList.markers.isEmpty {
+ newMarkersLists.add(listName: listName, list: newList)
+ }
+ }
+
+ return newMarkersLists
+ }
+
+ // Helper function to compare color components
+ private func colorComponentsMatch(_ color1: CGColor, _ color2: CGColor) -> Bool {
+ // Check if color spaces match
+ guard color1.colorSpace?.model == color2.colorSpace?.model else {
+ return false
+ }
+
+ // Get components
+ let components1 = color1.components ?? []
+ let components2 = color2.components ?? []
+
+ // Check if component counts match
+ guard components1.count == components2.count else {
+ return false
+ }
+
+ // Compare components with a small tolerance
+ let tolerance: CGFloat = 0.001
+ for i in 0.. tolerance {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ public func updateMarkers(markersLists: W3WMarkersLists) {
+ removeAllMarkers()
+ select(at: mapGridData?.selectedSquare ?? W3WBaseSquare())
+
+ let defaultColor = W3WColor(light: .darkBlue, dark: .white)
+ let defaultType: W3WMarkerType = .circle
+
+ let lists = markersLists.getLists()
+
+ for list in lists.values {
+ let color = list.color ?? defaultColor
+ let type = list.type ?? defaultType
+
+ for marker in list.markers {
+ addMarker(at: marker, color: color, type: type)
+ }
+ }
+ }
+
+ public func convertTo3wa(coordinates: CLLocationCoordinate2D, language: W3WLanguage = W3WBaseLanguage.english, completion: @escaping W3WSquareResponse ) {
+
+ self.w3w.convertTo3wa(coordinates: coordinates, language: language) { [weak self] square, error in
+ guard self != nil else { return }
+
+ if let error = error {
+ W3WThread.runOnMain {
+ completion(nil, error)
+ }
+ } else if let s = square {
+ W3WThread.runOnMain {
+ completion(s, nil)
+ }
+ }
+ }
+ }
+}
+
+extension W3WAppleMapHelper {
+
+ public func setRegion(_ region: MKCoordinateRegion, animated: Bool) {
+ mapView?.setRegion(region, animated: false)
+ }
+
+ public func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool) {
+ mapView?.setCenter(coordinate, animated: animated)
+ }
+}
diff --git a/Sources/W3WSwiftComponentsMapApple/Helper/W3WAppleMapHelperProtocol.swift b/Sources/W3WSwiftComponentsMapApple/Helper/W3WAppleMapHelperProtocol.swift
new file mode 100644
index 0000000..5cca029
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Helper/W3WAppleMapHelperProtocol.swift
@@ -0,0 +1,59 @@
+//
+// W3WAppleMapHelperProtocol.swift
+// w3w-swift-components-map-apple
+//
+// Created by Henry Ng on 17/12/24.
+//
+import Foundation
+import CoreLocation
+import W3WSwiftCore
+import W3WSwiftThemes
+import W3WSwiftComponentsMap
+
+public protocol W3WAppleMapHelperProtocol {
+
+ // put a what3words annotation on the map showing the address
+ func addMarker(at square: W3WSquare?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion)
+
+ func addMarker(at suggestion: W3WSuggestion?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion)
+ func addMarker(at words: String?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion)
+ func addMarker(at coordinate: CLLocationCoordinate2D?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion)
+ func addMarker(at squares: [W3WSquare]?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion)
+ func addMarker(at suggestions: [W3WSuggestion]?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion)
+ func addMarker(at words: [String]?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion)
+ func addMarker(at coordinates: [CLLocationCoordinate2D]?, color: W3WColor?, type: W3WMarkerType, completion: @escaping MarkerCompletion)
+
+ // remove what3words annotations from the map if they are present
+ func removeMarker(at suggestion: W3WSuggestion?)
+ func removeMarker(at words: String?)
+ func removeMarker(at squares: [W3WSquare]?)
+ func removeMarker(at suggestions: [W3WSuggestion]?)
+ func removeMarker(at words: [String]?)
+ func removeMarker(at square: W3WSquare?)
+ func removeMarker(group: String)
+
+ // show the "selected" outline around a square
+ func select(at: W3WSquare)
+
+ // remove the selection from the selected square
+ func unselect()
+
+ // show the "hover" outline around a square
+ func hover(at: CLLocationCoordinate2D)
+
+ // hide the "hover" outline around a square
+ func unhover()
+
+ // get the list of added squares
+ func getAllMarkers() -> [W3WSquare]
+
+ // remove what3words annotations from the map if they are present
+ func removeAllMarkers()
+
+ // find a marker by it's coordinates and return it if it exists in the map
+ func findMarker(by coordinates: CLLocationCoordinate2D) -> W3WSquare?
+
+ // sets the size of a square after .zoom is used in a show() call
+ func set(zoomInPointsPerSquare: CGFloat)
+
+}
diff --git a/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapAnnotation.swift b/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapAnnotation.swift
new file mode 100644
index 0000000..9a3aeb7
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapAnnotation.swift
@@ -0,0 +1,58 @@
+//
+// W3WAnnotation.swift
+// w3w-swift-components-map
+//
+// Created by Henry Ng on 13/1/25.
+//
+
+#if !os(macOS) && !os(watchOS)
+
+import Foundation
+import MapKit
+import W3WSwiftCore
+import W3WSwiftThemes
+import W3WSwiftComponentsMap
+
+public class W3WAppleMapAnnotation: MKPointAnnotation {
+
+ var square: W3WSquare?
+
+ var type: W3WMarkerType? = .circle
+
+ var color: W3WColor?
+
+ var isMarker: Bool?
+
+ var isMark: Bool?
+
+ var isSaved: Bool?
+
+
+ public init(square: W3WSquare, color: W3WColor? = nil, type: W3WMarkerType? = .circle, isMarker: Bool? = false, isMark: Bool? = false, isSaved: Bool? = false) {
+
+ super.init()
+
+ self.color = color
+ self.type = type
+ self.isMarker = isMarker
+ self.isMark = isMark
+ self.square = square
+ self.isSaved = isSaved
+
+ if let words = square.words {
+ if W3WSettings.leftToRight {
+ title = "///" + words
+ } else {
+ title = words + "///"
+ }
+ }
+
+ if let coordinates = square.coordinates {
+ self.coordinate = coordinates
+ }
+
+ }
+}
+
+
+#endif
diff --git a/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapGridData.swift b/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapGridData.swift
new file mode 100644
index 0000000..229dcc5
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapGridData.swift
@@ -0,0 +1,214 @@
+//
+// W3WAppleMapGridData.swift
+// w3w-swift-components-map-apple
+//
+// Created by Henry Ng on 18/1/25.
+//
+
+#if !os(macOS) && !os(watchOS)
+
+import Foundation
+import MapKit
+import W3WSwiftCore
+import W3WSwiftThemes
+import Combine
+import W3WSwiftComponentsMap
+
+
+public class W3WAppleMapGridData {
+
+ private var cancellables = Set()
+
+ let squareColor = W3WLive(.w3wBrandBase)
+
+ let mapGridColor = W3WLive(.mediumGrey)
+ let mapGridLineThickness = W3WLive(0.5)
+
+// let mapSquareColor = W3WLive(.black)
+ let mapSquareLineThickness = W3WLive(0.1)
+
+ let pinWidth = CGFloat(30.0)
+ let pinHeight = CGFloat(30.0)
+ let pinFrameSize = CGFloat(30.0)
+ let pinSquareSize = CGFloat(50.0)
+ let squarePinFrameSize = CGFloat(50.0)
+
+ public var onError: W3WMapErrorHandler = { _ in }
+
+ var gridRendererPointer: W3WMapGridRenderer? = nil
+ var squareRendererPointer: W3WMapSquaresRenderer? = nil
+ var gridLinePointer: W3WMapGridLines? = nil
+
+ var w3w: W3WProtocolV4?
+
+ /// language to use currently
+ var language: W3WLanguage = W3WSettings.defaultLanguage
+
+ /// highighted individual squares on the map
+ var squares = [W3WSquare]()
+
+ var markers = [W3WSquare]()
+
+ var savedList = W3WMarkersLists()
+
+ var selectedSquare: W3WSquare? = nil
+
+ var squareIsMarker: W3WSquare? = nil
+
+ var currentSquare: W3WSquare? = nil
+
+ var overlayColors: [Int64: W3WColor] = [:]
+
+ var previousStateHash: Int? = 0
+
+ var scheme: W3WScheme? = .w3w
+
+ var mapZoomLevel = CGFloat(0.0)
+
+ var pointsPerSquare = CGFloat(12.0)
+
+ /// keep track of the zoom level so we can change pins to squares at a certain point
+ var lastZoomPointsPerSquare = CGFloat(0.0)
+
+ var visibleZoomPointsPerSquare = CGFloat(32.0)
+
+ var gridRenderer: W3WMapGridRenderer? {
+ get { return gridRendererPointer }
+ set { gridRendererPointer = newValue }
+ }
+
+ var squareRenderer: W3WMapSquaresRenderer? {
+ get { return squareRendererPointer }
+ set { squareRendererPointer = newValue }
+ }
+
+
+ var gridLines: W3WMapGridLines? {
+ get { return gridLinePointer }
+ set { gridLinePointer = newValue }
+ }
+
+ var gridUpdateDebouncer = W3WDebouncer(delay: 0.3, closure: { _ in })
+
+ public init(w3w: W3WProtocolV4, scheme: W3WScheme? = .w3w, language: W3WLanguage = W3WSettings.defaultLanguage) {
+
+ self.w3w = w3w
+ self.scheme = scheme
+ self.language = language
+
+ self.mapGridColorListener()
+ self.squareColorListener()
+ self.squareLineThicknessListener()
+ self.gridLineThicknessListener()
+ }
+
+ private func mapGridColorListener() {
+ mapGridColor
+ .sink { [weak self] color in
+ self?.gridRenderer?.strokeColor = color.uiColor
+ }
+ .store(in: &cancellables)
+ }
+
+ private func squareColorListener() {
+ squareColor
+ .sink { [weak self] color in
+ self?.squareRendererPointer?.strokeColor = color.uiColor
+ }
+ .store(in: &cancellables)
+ }
+
+ private func squareLineThicknessListener() {
+ mapSquareLineThickness
+ .sink { [weak self] lineThickness in
+ self?.squareRendererPointer?.lineWidth = lineThickness.value
+ }
+ .store(in: &cancellables)
+ }
+
+ private func gridLineThicknessListener() {
+ mapGridLineThickness
+ .sink { [weak self] lineThickness in
+ self?.gridRenderer?.lineWidth = lineThickness.value
+ }
+ .store(in: &cancellables)
+ }
+
+ public func set(scheme: W3WScheme?) {
+ self.scheme = scheme
+ }
+
+ public func set(language: W3WLanguage) {
+ self.language = language
+ }
+
+}
+
+public class W3WMapGridLines: MKMultiPolyline {
+}
+
+public class W3WMapGridRenderer: MKMultiPolylineRenderer {
+}
+
+public class W3WMapSquareLines: MKPolyline {
+
+ var box: W3WBaseBox {
+ let points = self.points()
+ let sw = points[3].coordinate // SW is at index 3
+ let ne = points[1].coordinate // NE is at index 1
+ return W3WBaseBox(southWest: sw, northEast: ne)
+ }
+
+ convenience init? (bounds: W3WBaseBox?) {
+ guard let ne = bounds?.northEast,
+ let sw = bounds?.southWest else {
+ return nil
+ }
+
+ let nw = CLLocationCoordinate2D(latitude: ne.latitude, longitude: sw.longitude)
+ let se = CLLocationCoordinate2D(latitude: sw.latitude, longitude: ne.longitude)
+ let coordinates = [nw, ne, se, sw, nw]
+
+ self.init(coordinates: coordinates, count: 5)
+ }
+}
+
+public class W3WMapSquaresRenderer: MKPolylineRenderer {
+
+ private var squareW3WImage: UIImage?
+ // Cache the bounding rect to avoid recalculating it
+ private var cachedBoundingRect: CGRect?
+
+ override public func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
+ super.draw(mapRect, zoomScale: zoomScale, in: context)
+
+ // Only proceed with drawing if we have an image
+ guard let w3wImage = squareW3WImage else {
+ return
+
+ }
+ // Calculate the display rect if needed
+ let displayRect = cachedBoundingRect ?? {
+ let rect = self.rect(for: self.polyline.boundingMapRect)
+ let imageRect = rect.insetBy(dx: self.lineWidth, dy: self.lineWidth)
+ cachedBoundingRect = imageRect
+ return imageRect
+ }()
+
+ // Use more efficient drawing methods
+ UIGraphicsPushContext(context)
+ context.saveGState()
+ w3wImage.draw(in: displayRect, blendMode: .normal, alpha: 1.0)
+ context.restoreGState()
+ UIGraphicsPopContext()
+ }
+
+ // Method to set the W3WImage
+ public func setSquareImage(_ w3wImage: UIImage?) {
+ self.squareW3WImage = w3wImage
+ self.cachedBoundingRect = nil // Invalidate cached rect
+ self.setNeedsDisplay()
+ }
+}
+
+#endif
diff --git a/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapTypes.swift b/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapTypes.swift
new file mode 100644
index 0000000..acfd695
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Type/W3WAppleMapTypes.swift
@@ -0,0 +1,14 @@
+//
+// W3WMapError.swift
+// w3w-swift-components-map
+//
+// Created by Henry Ng on 2/1/25.
+//
+
+
+import W3WSwiftCore
+
+/// error response code block definition
+public typealias W3WMapErrorHandler = (W3WError) -> ()
+
+public typealias MarkerCompletion = (W3WSquare?, W3WError?) -> ()
diff --git a/Sources/W3WSwiftComponentsMapApple/Type/W3WAreaMath.swift b/Sources/W3WSwiftComponentsMapApple/Type/W3WAreaMath.swift
new file mode 100644
index 0000000..757d6f0
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Type/W3WAreaMath.swift
@@ -0,0 +1,62 @@
+//
+// W3WAearMath.swift
+// w3w-swift-components-map-apple
+//
+// Created by Henry Ng on 7/2/25.
+//
+
+
+import Foundation
+import CoreLocation
+
+
+/// given coordinates, find the center and span containing all of them
+class W3WAreaMath {
+
+ var middle = CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)
+ var count = 0
+ var minLat = Double.infinity
+ var minLng = Double.infinity
+ var maxLat = -Double.infinity
+ var maxLng = -Double.infinity
+
+
+ /// add a coordinate to the list
+ func add(coordinates: CLLocationCoordinate2D) {
+ if minLat > coordinates.latitude {
+ minLat = coordinates.latitude
+ }
+ if minLng > coordinates.longitude {
+ minLng = coordinates.longitude
+ }
+ if maxLat < coordinates.latitude {
+ maxLat = coordinates.latitude
+ }
+ if maxLng < coordinates.longitude {
+ maxLng = coordinates.longitude
+ }
+
+ count += 1
+ }
+
+
+ /// return the center of the group of all the coordinates
+ func getCenter() -> CLLocationCoordinate2D {
+ if count > 0 {
+ middle.latitude = (maxLat - minLat) / 2.0 + minLat
+ middle.longitude = (maxLng - minLng) / 2.0 + minLng
+ }
+
+ return middle
+ }
+
+
+ /// return the span from the center of the group in lat,lng
+ func getSpan() -> (Double, Double) {
+ let latSpan = min(max(-90.0, (maxLat - minLat) * 1.5), 90.0)
+ let longSpan = min(max(-180.0, (maxLng - minLng) * 1.5), 180.0)
+
+ return (latSpan, longSpan)
+ }
+
+}
diff --git a/Sources/W3WSwiftComponentsMapApple/Type/W3WColor+.swift b/Sources/W3WSwiftComponentsMapApple/Type/W3WColor+.swift
new file mode 100644
index 0000000..6008f25
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Type/W3WColor+.swift
@@ -0,0 +1,21 @@
+//
+// W3WColor+.swift
+// w3w-swift-components-map-apple
+//
+// Created by Henry Ng on 25/4/25.
+//
+import W3WSwiftThemes
+import W3WSwiftCore
+
+extension W3WColor: Equatable {
+
+ public static func == (lhs: W3WColor, rhs: W3WColor) -> Bool {
+
+ guard lhs.colors.count == rhs.colors.count else { return false }
+
+ for(mode, color) in lhs.colors {
+ guard let rhsColor = rhs.colors[mode], rhsColor == color else { return false }
+ }
+ return true
+ }
+}
diff --git a/Sources/W3WSwiftComponentsMapApple/Type/W3WImageCache.swift b/Sources/W3WSwiftComponentsMapApple/Type/W3WImageCache.swift
new file mode 100644
index 0000000..bbcd210
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Type/W3WImageCache.swift
@@ -0,0 +1,34 @@
+//
+// W3WImageCache.swift
+// w3w-swift-components-map-apple
+//
+// Created by Henry Ng on 3/3/25.
+//
+
+import UIKit
+import W3WSwiftThemes
+
+
+class W3WImageCache {
+ static let shared = W3WImageCache()
+ private var cache = NSCache()
+
+ func getImage(for color: W3WColor, size: CGSize) -> UIImage? {
+ let key = "\(color.description)_\(size.width)_\(size.height)" as NSString
+
+ if let cachedImage = cache.object(forKey: key) {
+ return cachedImage
+ }
+
+ // Get the image
+ let newImage = W3WImage(drawing: .mapSquare, colors: .standardMaps.with(background: color) .with(foreground: color.complimentaryTextColor()))
+ .get(size: W3WIconSize(value: size))
+
+ // Cache it if it's not nil
+ cache.setObject(newImage, forKey: key)
+
+ return newImage
+ }
+}
+
+
diff --git a/Sources/W3WSwiftComponentsMapApple/Type/W3WSettings.swift b/Sources/W3WSwiftComponentsMapApple/Type/W3WSettings.swift
new file mode 100644
index 0000000..4302925
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/Type/W3WSettings.swift
@@ -0,0 +1,13 @@
+//
+// W3WSettings.swift
+// w3w-swift-components-map-apple
+//
+// Created by Henry Ng on 6/5/25.
+//
+
+import W3WSwiftCore
+
+extension W3WSettings {
+ public static var maxMetersDiagonalForGrid = 4000.0
+
+}
diff --git a/Sources/W3WSwiftComponentsMapApple/View/W3WAppleMapView.swift b/Sources/W3WSwiftComponentsMapApple/View/W3WAppleMapView.swift
new file mode 100644
index 0000000..00c35bc
--- /dev/null
+++ b/Sources/W3WSwiftComponentsMapApple/View/W3WAppleMapView.swift
@@ -0,0 +1,244 @@
+//
+// W3WAppleMapView.swift
+//
+//
+// Created on 03/12/2024.
+//
+
+import MapKit
+import W3WSwiftCore
+import W3WSwiftComponentsMap
+import W3WSwiftCore
+import W3WSwiftDesign
+
+/// An Apple Map Kit Map
+public class W3WAppleMapView: MKMapView, UIGestureRecognizerDelegate, W3WMapViewProtocol, W3WEventSubscriberProtocol {
+
+ public var transitionScale = W3WMapScale(pointsPerMeter: CGFloat(4.0))
+
+ public var subscriptions = W3WEventsSubscriptions()
+
+ /// The map view model to use
+ public var viewModel: W3WMapViewModelProtocol
+
+ var helper: W3WAppleMapDrawerProtocol!
+
+ typealias W3WHelper = W3WAppleMapHelper
+
+ private var w3wHelper: W3WHelper { helper as! W3WHelper }
+
+ private var onError: W3WMapErrorHandler = { _ in }
+
+ /// The available map types
+ public var types: [W3WMapType] { get { return [.standard, .satellite, .hybrid] } }
+
+ /// Make an Apple Map Kit Map
+ /// - Parameters
+ /// - viewModel: The viewModel to use
+ public init(viewModel: W3WMapViewModelProtocol) {
+
+ self.viewModel = viewModel
+ super.init(frame: .w3wWhatever)
+ self.helper = W3WAppleMapHelper(mapView: self, viewModel.w3w) as! any W3WAppleMapDrawerProtocol
+
+ configure()
+ }
+
+ func configure() {
+
+ delegate = self
+
+ set(viewModel: self.viewModel)
+
+ set(type: .hybrid)
+
+ bind()
+
+ attachTapRecognizer()
+
+ }
+
+ /// Make an Apple Map Kit Map
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ /// Change the viewModel for this map. Typically used when
+ /// switching maps in the map view
+ /// - Parameters
+ /// - viewModel: The viewModel to use
+ public func set(viewModel: W3WMapViewModelProtocol) {
+ self.viewModel = viewModel
+ }
+
+ public func set(type: String) {
+ w3wHelper.set(type: type)
+ }
+
+ public func getType() -> W3WMapType {
+
+ let type = w3wHelper.getType()
+ switch type {
+ case .standard: return "standard"
+ case .satellite: return "satellite"
+ case .hybrid: return "hybridFlyover"
+
+ default: return "hybridFlyover"
+ }
+ }
+
+ public func getCameraState() -> W3WMapCamera {
+ let mapView = w3wHelper.mapView
+ return
+ W3WMapCamera(center: mapView?.region.center, scale: W3WMapScale(span: mapView!.region.span , mapSize: mapView!.frame.size))
+ }
+
+ public func set(scheme: W3WScheme?) {
+ w3wHelper.set(scheme: scheme)
+ }
+
+ public func updateSavedLists(markers: W3WMarkersLists) {
+ viewModel.input.markers.send(markers)
+ }
+
+ func bind() {
+
+ subscribe(to: self.viewModel.input.markers) { [weak self] markers in
+ guard let self = self else { return }
+
+ if !markers.getLists().isEmpty {
+ w3wHelper.updateMarkers(markersLists: markers)
+ }
+ }
+
+ subscribe(to: self.viewModel.input.camera) { [weak self] camera in
+ guard let self = self else { return }
+ w3wHelper.updateCamera(camera: camera)
+ }
+
+ subscribe(to: self.viewModel.input.selected) { [weak self] square in
+ guard let self = self else { return }
+ w3wHelper.updateSquare(square: square)
+ }
+
+ subscribe(to: self.viewModel.gps) { [weak self] gps in
+ guard let self = self else { return }
+ }
+
+ }
+
+ func attachTapRecognizer() {
+
+ let mapView = w3wHelper.mapView
+
+ let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
+ tap.numberOfTapsRequired = 1
+ tap.numberOfTouchesRequired = 1
+
+ let doubleTap = UITapGestureRecognizer(target: self, action:nil)
+ doubleTap.numberOfTapsRequired = 2
+ mapView?.addGestureRecognizer(doubleTap)
+ tap.require(toFail: doubleTap)
+
+
+ tap.delegate = self
+
+ mapView?.addGestureRecognizer(tap)
+ }
+
+ public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
+ if isNotPartofTheMap(view: touch.view) {
+ return false
+ } else {
+ return true
+ }
+ }
+
+
+ func isNotPartofTheMap(view: UIView?) -> Bool {
+ if view == nil {
+ return false
+ } else if view is MKAnnotationView || view is UIButton {
+ return true
+ } else {
+ return isNotPartofTheMap(view: view?.superview)
+ }
+ }
+
+ @objc func tapped(_ gestureRecognizer : UITapGestureRecognizer) {
+ let mapView = w3wHelper.mapView
+ let location = gestureRecognizer.location(in: mapView)
+
+ if let coordinates = mapView?.convert(location, toCoordinateFrom: mapView) {
+ self.w3wHelper.select(at: coordinates) { [weak self] result in
+
+ guard let self = self else { return }
+ switch result {
+ case .success(let square):
+ self.viewModel.output.send(.selected(square))
+ case .failure(let error):
+ print("Show Error")
+ default: break
+ }
+ }
+ }
+ }
+
+}
+
+extension W3WAppleMapView: MKMapViewDelegate {
+
+ // MARK: UIMapViewDelegates
+ public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
+ let currentMapScale = W3WMapScale(span: mapView.region.span, mapSize: mapView.frame.size)
+
+ w3wHelper.mapViewDidChangeVisibleRegion(mapView)
+ viewModel.output.send(.camera(getCameraState()))
+
+ }
+
+ public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
+
+ if let w3wOverlay = w3wHelper.mapRenderer(overlay: overlay) {
+ return w3wOverlay
+ }
+ return MKOverlayRenderer()
+
+ }
+
+ /// delegate callback to provide a cusomt annotation view
+ public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
+ return w3wHelper.mapView(mapView, viewFor: annotation, with: self.transitionScale.value)
+ }
+
+ public func mapView(_ mapView: MKMapView, didAdd renderers: [MKOverlayRenderer]) {
+ w3wHelper.mapView(mapView, didAdd: renderers)
+ }
+
+ public func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
+ w3wHelper.mapView(mapView, didAdd: views)
+ }
+
+ public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
+ w3wHelper.mapView(mapView, regionWillChangeAnimated: animated)
+ }
+
+ public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
+
+ w3wHelper.mapView(mapView, regionDidChangeAnimated: animated)
+
+ }
+
+ public func mapView(_ mapView: MKMapView, mapTypeChanged type: MKMapType) {
+ w3wHelper.mapView(mapView, mapTypeChanged: type)
+ }
+
+ //when marker is being selected
+ public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
+ if let markerView = view.annotation as? W3WAppleMapAnnotation {
+
+ }
+ w3wHelper.mapView(mapView, didSelect: view)
+ }
+
+}
diff --git a/Sources/W3WSwiftComponentsMapApple/W3WAppleMapView.swift b/Sources/W3WSwiftComponentsMapApple/W3WAppleMapView.swift
deleted file mode 100644
index b3245b9..0000000
--- a/Sources/W3WSwiftComponentsMapApple/W3WAppleMapView.swift
+++ /dev/null
@@ -1,56 +0,0 @@
-//
-// W3WAppleMapView.swift
-//
-//
-// Created on 03/12/2024.
-//
-
-import MapKit
-import W3WSwiftCore
-import W3WSwiftComponentsMap
-
-
-/// An Apple Map Kit Map
-public class W3WAppleMapView: MKMapView, W3WMapViewProtocol, W3WEventSubscriberProtocol, MKMapViewDelegate {
- public var subscriptions = W3WEventsSubscriptions()
-
- /// The map view model to use
- public var viewModel: W3WMapViewModelProtocol
-
- /// The available map types
- public var types: [W3WMapType] { get { return [.standard, .satellite, .hybrid, "silly"] } }
-
-
- /// Make an Apple Map Kit Map
- /// - Parameters
- /// - viewModel: The viewModel to use
- public init(viewModel: W3WMapViewModelProtocol) {
- self.viewModel = viewModel
- super.init(frame: .w3wWhatever)
- }
-
-
- /// Make an Apple Map Kit Map
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
-
- /// Change the viewModel for this map. Typically used when
- /// switching maps in the map view
- /// - Parameters
- /// - viewModel: The viewModel to use
- public func set(viewModel: W3WMapViewModelProtocol) {
- self.viewModel = viewModel
- }
-
-
- /// Change the map type, there is a convenince function in W3WMapViewProtocol
- /// that accepts a W3WMapType, and calls this. Common map types are defined
- /// there
- /// - Parameters
- /// - type: A string type from the array `self.types`
- public func set(type: String) {
- }
-
-}