Files
MapLibreParticleLab/platform/ios/app-swift/Sources/Stability/UserMap.swift
2026-07-18 00:19:15 +00:00

277 lines
9.4 KiB
Swift

import MapLibre
class UserMap: MLNMapView, MLNMapViewDelegate {
private var continuation: CheckedContinuation<Void, Never>?
// config
private var RANDOM = RandomNumberGeneratorWithSeed(seed: 42)
private let STYLES = [
DEMOTILES_STYLE,
AMERICANA_STYLE,
OPENFREEMAP_LIBERTY_STYLE,
OPENFREEMAP_BRIGHT_STYLE,
PROTOMAPS_LIGHT_STYLE,
PROTOMAPS_DARK_STYLE,
PROTOMAPS_GRAYSCALE_STYLE,
PROTOMAPS_WHITE_STYLE,
PROTOMAPS_BLACK_STYLE,
]
private let PLACES = [
CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), // SF
CLLocationCoordinate2D(latitude: 38.9072, longitude: -77.0369), // DC
CLLocationCoordinate2D(latitude: 52.3702, longitude: 4.8952), // AMS
CLLocationCoordinate2D(latitude: 60.1699, longitude: 24.9384), // HEL
CLLocationCoordinate2D(latitude: -13.1639, longitude: -74.2236), // AYA
CLLocationCoordinate2D(latitude: 52.5200, longitude: 13.4050), // BER
CLLocationCoordinate2D(latitude: 12.9716, longitude: 77.5946), // BAN
CLLocationCoordinate2D(latitude: 31.2304, longitude: 121.4737), // SHA
]
func random(in range: ClosedRange<Int>) -> Int {
Int.random(in: range, using: &RANDOM)
}
func random(in range: ClosedRange<Double>) -> Double {
Double.random(in: range, using: &RANDOM)
}
func weightedRandom<T>(in values: [(T, Double)]) -> T {
let cumulativeWeights = values.reduce(into: (total: 0.0, result: [Double]())) { acc, value in
acc.total += value.1
acc.result.append(acc.total)
}.result
let randomWeight = random(in: 0.0 ... cumulativeWeights.last!)
var low = 0
var high = cumulativeWeights.count - 1
while low < high {
let mid = (low + high) / 2
if randomWeight < cumulativeWeights[mid] {
high = mid
} else {
low = mid + 1
}
}
return values[low].0
}
func randomSlowDuration() -> TimeInterval { random(in: 3000 ... 5000) / 1000.0 }
func randomFastDuration() -> TimeInterval { random(in: 500 ... 1000) / 1000.0 }
func randomPlacePoints() -> Int { random(in: 10 ... 20) }
func randomPlaceActions() -> Int { random(in: 10 ... 20) }
func randomLatLng() -> CLLocationCoordinate2D {
let bounds = visibleCoordinateBounds
return CLLocationCoordinate2D(
latitude: random(in: bounds.sw.latitude ... bounds.ne.latitude),
longitude: random(in: bounds.sw.longitude ... bounds.ne.longitude)
)
}
func randomZoom() -> Double { random(in: 14.0 ... 20.0) }
func randomAltitude() -> Double {
MLNAltitudeForZoomLevel(randomZoom(), camera.pitch, camera.centerCoordinate.latitude, frame.size)
}
func randomTilt() -> Double { random(in: 0.0 ... 60.0) }
func randomBearing() -> Double { random(in: 0.0 ... 360.0) }
func randomAnnotationRemove() -> Int { random(in: 4 ... 8) }
func randomAnnotationAdd() -> Int { random(in: 2 ... 4) }
func randomPolyPoints() -> [CLLocationCoordinate2D] {
let bounds = visibleCoordinateBounds
let boundMultiplier = 0.25
let lat = random(in: bounds.sw.latitude ... bounds.ne.latitude)
let lng = random(in: bounds.sw.longitude ... bounds.ne.longitude)
let latRange = abs(bounds.ne.latitude - bounds.sw.latitude) * boundMultiplier / 2.0
let lngRange = abs(bounds.ne.longitude - bounds.sw.longitude) * boundMultiplier / 2.0
return (0 ... random(in: 3 ... 15)).map { _ in
CLLocationCoordinate2D(
latitude: lat + random(in: -latRange ... latRange),
longitude: lng + random(in: -lngRange ... lngRange)
)
}
}
func randomLineWidth() -> Double { random(in: 1.0 ... 4.0) }
func randomColor() -> UIColor {
UIColor(
red: CGFloat(random(in: 0.0 ... 1.0)),
green: CGFloat(random(in: 0.0 ... 1.0)),
blue: CGFloat(random(in: 0.0 ... 1.0)),
alpha: CGFloat(random(in: 0.5 ... 1.0))
)
}
@MainActor func load(style: URL) async {
styleURL = style
return await withCheckedContinuation { continuation in
guard self.style == nil else {
self.continuation = nil
continuation.resume()
return
}
self.continuation = continuation
}
}
func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) {
continuation?.resume()
continuation = nil
}
func mapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? {
guard annotation is MLNPointAnnotation else {
return nil
}
let reuseIdentifier = "(\(annotation.coordinate.latitude),\(annotation.coordinate.longitude))"
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
if annotationView != nil {
return annotationView
}
class AnnotationView: MLNAnnotationView {
var map: UserMap
init(map: UserMap, reuseIdentifier: String) {
self.map = map
super.init(reuseIdentifier: reuseIdentifier)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.width / 2
layer.borderWidth = 2
layer.borderColor = UIColor.white.cgColor
layer.backgroundColor = map.randomColor().cgColor
}
}
let annotation = AnnotationView(map: self, reuseIdentifier: reuseIdentifier)
annotation.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
return annotation
}
func mapView(_: MLNMapView, strokeColorForShapeAnnotation _: MLNShape) -> UIColor {
randomColor()
}
func mapView(_: MLNMapView, lineWidthForPolylineAnnotation _: MLNPolyline) -> CGFloat {
randomLineWidth()
}
func mapView(_: MLNMapView, fillColorForPolygonAnnotation _: MLNPolygon) -> UIColor {
randomColor()
}
func run() {
delegate = self
Task {
while true {
await runStyleActions()
}
}
}
@MainActor func runStyleActions() async {
for style in STYLES.shuffled(using: &RANDOM) {
await load(style: style!)
await runCameraActions()
}
}
@MainActor func runCameraActions() async {
// camera actions with different weights
// position updates are more frequent
let actions = [
({ [weak self] () in let cam = self!.camera; cam.centerCoordinate = self!.randomLatLng(); return cam }, 2.0),
({ [weak self] () in let cam = self!.camera; cam.altitude = self!.randomAltitude(); return cam }, 1.0),
({ [weak self] () in let cam = self!.camera; cam.pitch = self!.randomTilt(); return cam }, 1.0),
({ [weak self] () in let cam = self!.camera; cam.heading = self!.randomBearing(); return cam }, 1.0),
]
for placeCenter in PLACES.shuffled(using: &RANDOM) {
// update all values to simulate a long jump
// (generated by the app, searching for a city/street, etc)
let camera = MLNMapCamera(
lookingAtCenter: placeCenter,
altitude: randomAltitude(),
pitch: randomTilt(),
heading: randomBearing()
)
await animate(camera: camera, withDuration: randomSlowDuration())
await randomPlacePoints().repeat {
// perform a series of fast camera actions
await randomPlaceActions().repeat {
// update each value individually to simulate user interaction
await self.animate(camera: weightedRandom(in: actions)(), withDuration: randomFastDuration())
}
runAnnotationActions()
}
}
removeAnnotations(annotations ?? [])
}
func runAnnotationActions() {
// remove some annotations
min(annotations?.count ?? 0, randomAnnotationRemove()).repeat {
self.removeAnnotation((self.annotations!.randomElement(using: &RANDOM)!))
}
// add some annotations
// points
randomAnnotationAdd().repeat {
let annotation = MLNPointAnnotation()
annotation.coordinate = randomLatLng()
annotation.title = "(\(annotation.coordinate.latitude),\(annotation.coordinate.longitude))"
self.addAnnotation(annotation)
}
// polylines
randomAnnotationAdd().repeat {
let coords = randomPolyPoints()
let annotation = MLNPolyline(coordinates: coords, count: UInt(coords.count))
// random color/width set via delegate
self.addAnnotation(annotation)
}
// polygons
randomAnnotationAdd().repeat {
let coords = randomPolyPoints()
let annotation = MLNPolygon(coordinates: coords, count: UInt(coords.count))
// random fill/stroke colors set via delegate
self.addAnnotation(annotation)
}
}
}