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) { - } - -}