Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/map camera and layer mods #12

Merged
merged 5 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ let package = Package(
),
.binaryTarget(
name: "MapLibre",
url: "https://github.com/maplibre/maplibre-native/releases/download/ios-v6.0.0-preda45706601c7ccc6d922a8fcddfc62ff7c8f480d/MapLibre.dynamic.xcframework.zip",
checksum: "37e621c0c7c1f589f0a125816155ba443000d78b80649d85a9b8b3d19144836c"
url: "https://github.com/maplibre/maplibre-native/releases/download/ios-v6.0.0-pre9fbcb031f019048f21fdfcb57b80f4451cdecfd9/MapLibre.dynamic.xcframework.zip",
checksum: "929bbc3f24740df360bf447acf08bd102cf4baf12a6bc099f42e5fb8b5130cf4"
),
.target(
name: "MapLibreSwiftUI",
Expand Down
34 changes: 34 additions & 0 deletions Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

@resultBuilder
public enum MapViewContentBuilder {

public static func buildBlock(_ layers: [StyleLayerDefinition]...) -> [StyleLayerDefinition] {
return layers.flatMap { $0 }
}

public static func buildOptional(_ layers: [StyleLayerDefinition]?) -> [StyleLayerDefinition] {
return layers ?? []
}

public static func buildExpression(_ layer: StyleLayerDefinition) -> [StyleLayerDefinition] {
return [layer]
}

public static func buildExpression(_ expression: Void) -> [StyleLayerDefinition] {
return []
}

public static func buildExpression(_ styleCollection: StyleLayerCollection) -> [StyleLayerDefinition] {
return styleCollection.layers
}


public static func buildEither(first layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return layer
}

public static func buildEither(second layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return layer
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

public protocol StyleLayerCollection {

@MapViewContentBuilder var layers: [StyleLayerDefinition] { get }
}
21 changes: 11 additions & 10 deletions Sources/MapLibreSwiftUI/Examples/Camera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,27 @@ import SwiftUI
private let switzerland = CLLocationCoordinate2D(latitude: 46.801111, longitude: 8.226667)

struct CameraDirectManipulationPreview: View {
@State private var camera = MapView.Camera.centerAndZoom(switzerland, 4)
@State private var camera = MapViewCamera.center(switzerland, zoom: 4)

let styleURL: URL

var body: some View {
MapView(styleURL: styleURL, camera: $camera)
.overlay(alignment: .bottom, content: {
switch camera {
case .centerAndZoom(let coord, let zoom):
Text("\(coord.latitude), \(coord.longitude) z \(zoom ?? 0)")
.padding()
.background(in: RoundedRectangle(cornerRadius: 8),
fillStyle: .init())
.padding(.bottom, 42)
}
Text("\(camera.coordinate.latitude), \(camera.coordinate.longitude) z \(camera.zoom ?? 0)")
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved
.padding()
.background(
Rectangle()
.foregroundColor(.black)
.cornerRadius(8)
)

ianthetechie marked this conversation as resolved.
Show resolved Hide resolved
.padding(.bottom, 42)
})
.task {
try! await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)

camera = MapView.Camera.centerAndZoom(switzerland, 6)
camera = MapViewCamera.center(switzerland, zoom: 6)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapLibreSwiftUI/Examples/Polyline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ struct PolylinePreview: View {
let styleURL: URL

var body: some View {
MapView(styleURL: styleURL, initialCamera: MapView.Camera.centerAndZoom(samplePedestrianWaypoints.first!, 14)) {
MapView(styleURL: styleURL, initialCamera: MapViewCamera.center(samplePedestrianWaypoints.first!, zoom: 14)) {
// Note: This line does not add the source to the style as if it
// were a statement in an imperative programming language.
// The source is added automatically if a layer references it.
Expand Down
70 changes: 33 additions & 37 deletions Sources/MapLibreSwiftUI/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@ import SwiftUI
import InternalUtils
import MapLibre
import MapLibreSwiftDSL

import MapLibreSwiftUI
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved

public struct MapView: UIViewRepresentable {
// TODO: Support MLNStyle as well; having a DSL for that would be nice
enum MapStyleSource {
case url(URL)
}

public enum Camera {
case centerAndZoom(CLLocationCoordinate2D, Double?)
}

var camera: Binding<Camera>?

public private(set) var camera: Binding<MapViewCamera>?

let styleSource: MapStyleSource
let userLayers: [StyleLayerDefinition]
public let styleSource: MapStyleSource
public let userLayers: [StyleLayerDefinition]
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved

public init(styleURL: URL, camera: Binding<Camera>? = nil, @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }) {
public init(
styleURL: URL,
camera: Binding<MapViewCamera>? = 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)
}

Expand All @@ -36,7 +36,8 @@ public struct MapView: UIViewRepresentable {
// Storage of variables as they were previously; these are snapshot
// every update cycle so we can avoid unnecessary updates
private var snapshotUserLayers: [StyleLayerDefinition] = []

var snapshotCamera: MapViewCamera?
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved

init(parent: MapView) {
self.parent = parent
}
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -161,7 +163,7 @@ public struct MapView: UIViewRepresentable {
mapView.styleURL = styleURL
}

updateMapCamera(mapView, animated: false)
updateMapCamera(mapView, context: context, animated: false)

// TODO: Make this settable via a modifier
mapView.logoView.isHidden = true
Expand All @@ -182,28 +184,22 @@ public struct MapView: UIViewRepresentable {
// FIXME: This isn't exactly telling us if the *map* is loaded, and the docs for setCenter say it needs t obe.
let isStyleLoaded = mapView.style != nil

updateMapCamera(mapView, animated: isStyleLoaded)
updateMapCamera(mapView, context: context, animated: isStyleLoaded)
}

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)
}
}
private func updateMapCamera(_ mapView: MLNMapView, context: Context, animated: Bool) {
guard let newCamera = self.camera?.wrappedValue,
context.coordinator.snapshotCamera != newCamera else {
// Exit early - the camera has not changed.
return
}
}
}

@resultBuilder
public enum MapViewContentBuilder {
public static func buildBlock(_ layers: StyleLayerDefinition...) -> [StyleLayerDefinition] {
return layers
mapView.setCenter(newCamera.coordinate,
zoomLevel: newCamera.zoom,
direction: newCamera.course,
animated: animated)
context.coordinator.snapshotCamera = newCamera
}
}

Expand Down
37 changes: 37 additions & 0 deletions Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved

/// 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
}
}
}
69 changes: 69 additions & 0 deletions Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift
Original file line number Diff line number Diff line change
@@ -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 {
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
6 changes: 6 additions & 0 deletions Sources/MapLibreSwiftUI/Models/MapStyleSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

// TODO: Support MLNStyle as well; having a DSL for that would be nice
public enum MapStyleSource {
case url(URL)
}
Loading