From 99504f6017bb0823b7f028934a49af6b03c597da Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Sat, 16 Dec 2023 17:14:19 -0800 Subject: [PATCH] Added map camera, layer mods, and more --- .../xcschemes/MapLibreSwiftDSL.xcscheme | 2 +- .../MapLibreSwiftUI-Package.xcscheme | 2 +- .../xcschemes/MapLibreSwiftUI.xcscheme | 2 +- Package.swift | 4 +- Sources/MapLibreSwiftUI/Examples/Camera.swift | 29 ++++---- .../MapLibreSwiftUI/Examples/Polyline.swift | 2 +- Sources/MapLibreSwiftUI/MapView.swift | 53 +++++++------- .../Models/MapCamera/CameraState.swift | 44 ++++++++++++ .../Models/MapCamera/MapViewCamera.swift | 69 +++++++++++++++++++ .../MapStyleSource/MapStyleSource.swift | 13 ++++ .../MapViewContentViewModifier.swift | 22 ++++++ 11 files changed, 195 insertions(+), 47 deletions(-) create mode 100644 Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift create mode 100644 Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift create mode 100644 Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift create mode 100644 Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftDSL.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftDSL.xcscheme index 0bdca09..a45b7e5 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftDSL.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftDSL.xcscheme @@ -1,6 +1,6 @@ ? - var camera: Binding? - - let styleSource: MapStyleSource + public let styleSource: MapStyleSource let userLayers: [StyleLayerDefinition] - public init(styleURL: URL, camera: Binding? = nil, @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }) { + public init( + styleURL: URL, + camera: Binding? = nil, + @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] } + ) { self.styleSource = .url(styleURL) self.camera = camera userLayers = makeMapContent() } - public init(styleURL: URL, initialCamera: Camera, @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }) { + public init( + styleURL: URL, + initialCamera: MapViewCamera, + @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] } + ) { self.init(styleURL: styleURL, camera: .constant(initialCamera), makeMapContent) } @@ -58,7 +59,8 @@ public struct MapView: UIViewRepresentable { public func mapView(_ mapView: MLNMapView, regionDidChangeAnimated animated: Bool) { DispatchQueue.main.async { - self.parent.camera?.wrappedValue = .centerAndZoom(mapView.centerCoordinate, mapView.zoomLevel) + self.parent.camera?.wrappedValue = .center(mapView.centerCoordinate, + zoom: mapView.zoomLevel) } } @@ -186,17 +188,18 @@ public struct MapView: UIViewRepresentable { } private func updateMapCamera(_ mapView: MLNMapView, animated: Bool) { - if let camera = self.camera { - switch camera.wrappedValue { - case .centerAndZoom(let center, let zoom): - // TODO: Determine if MapLibre is smart enough to keep animating to the same place multiple times; if not, add a check here to prevent suprious updates. - if let z = zoom { - mapView.setCenter(center, zoomLevel: z, animated: animated) - } else { - mapView.setCenter(center, animated: animated) - } - } + guard let newCamera = self.camera?.wrappedValue, + lastCamera != newCamera else { + // Exit early - the camera has not changed. + return } + + mapView.setCenter(newCamera.coordinate, + zoomLevel: newCamera.zoom, + direction: newCamera.course, + animated: animated) + + self.lastCamera = newCamera } } diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift new file mode 100644 index 0000000..f3bebac --- /dev/null +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift @@ -0,0 +1,44 @@ +// +// File.swift +// +// +// Created by Jacob Fielding on 12/16/23. +// + +import Foundation +import MapLibre + +/// The CameraState is used to understand the current context of the MapView's camera. +public enum CameraState { + + /// Centered on a coordinate + case centered + + /// The camera is currently following a location provider. + case userLocation + + /// Centered on a bounding box/rectangle. + case rect + + /// Showcasing a GeoJSON/Polygon + case showcase +} + +extension CameraState: Equatable { + + public static func ==(lhs: CameraState, rhs: CameraState) -> Bool { + switch (lhs, rhs) { + + case (.centered, .centered): + return true + case (.userLocation, .userLocation): + return true + case (.rect, .rect): + return true + case (.showcase, .showcase): + return true + default: + return false + } + } +} diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift new file mode 100644 index 0000000..8e977da --- /dev/null +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift @@ -0,0 +1,69 @@ +import Foundation +import CoreLocation + +public struct MapViewCamera { + + public var state: CameraState + public var coordinate: CLLocationCoordinate2D + public var zoom: Double + public var pitch: Double + public var course: CLLocationDirection + + /// A backup camera centered at 0.0, 0.0. This is typically used as a backup, + /// pre-load for an expected camera update (e.g. before a location provider produces + /// it's first location). + /// + /// - Returns: The constructed MapViewCamera. + public static func backup() -> MapViewCamera { + return MapViewCamera(state: .centered, + coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0), + zoom: 10, + pitch: 90, + course: 0) + } + + /// Center the map on a specific location. + /// + /// - Parameters: + /// - coordinate: The coordinate to center the map on. + /// - zoom: The zoom level. + /// - pitch: The camera pitch. Default is 90 (straight down). + /// - course: The course. Default is 0 (North). + /// - Returns: The constructed MapViewCamera. + public static func center(_ coordinate: CLLocationCoordinate2D, + zoom: Double, + pitch: Double = 90.0, + course: Double = 0) -> MapViewCamera { + + return MapViewCamera(state: .centered, + coordinate: coordinate, + zoom: zoom, + pitch: pitch, + course: course) + } + + public static func userLocation(_ location: CLLocation, + zoom: Double, + pitch: Double = 90.0) -> MapViewCamera { + + return MapViewCamera(state: .userLocation, + coordinate: location.coordinate, + zoom: zoom, + pitch: pitch, + course: location.course) + } + + // TODO: Create init methods for other camera states once supporting materials are understood (e.g. BoundingBox) +} + +extension MapViewCamera: Equatable { + + public static func ==(lhs: MapViewCamera, rhs: MapViewCamera) -> Bool { + return lhs.state == rhs.state + && lhs.coordinate.latitude == rhs.coordinate.latitude + && lhs.coordinate.longitude == rhs.coordinate.longitude + && lhs.zoom == rhs.zoom + && lhs.pitch == rhs.pitch + && lhs.course == rhs.course + } +} diff --git a/Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift b/Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift new file mode 100644 index 0000000..fb770e3 --- /dev/null +++ b/Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift @@ -0,0 +1,13 @@ +// +// File.swift +// +// +// Created by Jacob Fielding on 12/16/23. +// + +import Foundation + +// TODO: Support MLNStyle as well; having a DSL for that would be nice +public enum MapStyleSource { + case url(URL) +} diff --git a/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift b/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift new file mode 100644 index 0000000..23a788d --- /dev/null +++ b/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift @@ -0,0 +1,22 @@ +// +// File.swift +// +// +// Created by Jacob Fielding on 12/16/23. +// + +import SwiftUI +import MapLibreSwiftDSL + +extension MapView { + + public func mapContent(@MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition]) -> MapView { + switch self.styleSource { + + case .url(let styleUrl): + return MapView(styleURL: styleUrl, + camera: self.camera, + makeMapContent) + } + } +}