iOS long running test v2 (#3978)

Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
Adrian Cojocaru
2025-11-28 23:33:49 +02:00
committed by GitHub
parent b930e57e8b
commit cc33527dd8
23 changed files with 23349 additions and 16 deletions

View File

@@ -250,7 +250,7 @@ jobs:
--test_output=errors --local_test_jobs=1 \
--repo_env=GCOV="$PWD/.github/scripts/gcov_wrapper" \
--test_env=DISPLAY --test_env=XAUTHORITY --copt="-DCI_BUILD" \
//test:core //render-test:render-test //expression-test:test
//test:core //render-test:render-test //expression-test:test --//:renderer=drawable
echo coverage_report="$(bazel info output_path)"/_coverage/_coverage_report.dat >> "$GITHUB_ENV"
- name: Upload coverage report

View File

@@ -16,7 +16,7 @@ repos:
exclude: 'gradlew\.bat$'
- id: trailing-whitespace
# FIXME: these autogenerate files contain trailing whitespace. Need to fix generator.
exclude: '(platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/(location/LocationIndicatorLayer|style/layers/PropertyFactory)\.java$)|(platform/windows/vendor/.*)'
exclude: '(platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/(location/LocationIndicatorLayer|style/layers/PropertyFactory)\.java$)|(platform/windows/vendor/.*)|(platform/ios/vendor/.*)'
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.8

View File

@@ -144,7 +144,6 @@ cc_library(
"//vendor:eternal",
"//vendor:expected-lite",
"//vendor:maplibre-native-base",
"//vendor/maplibre-tile-spec/cpp:mlt_cpp",
"//vendor:parsedate",
"//vendor:pmtiles",
"//vendor:polylabel",
@@ -155,6 +154,7 @@ cc_library(
"//vendor:unordered_dense",
"//vendor:vector-tile",
"//vendor:wagyu",
"//vendor/maplibre-tile-spec/cpp:mlt_cpp",
] + select({
"@platforms//os:ios": [
"//vendor:ghc_filesystem",

View File

@@ -11,6 +11,7 @@ bazel_dep(name = "libuv", version = "1.48.0")
bazel_dep(name = "apple_support", version = "1.24.5", repo_name = "build_bazel_apple_support")
bazel_dep(name = "rules_cc", version = "0.2.14")
bazel_dep(name = "rules_shell", version = "0.6.1")
bazel_dep(name = "rules_swift_package_manager", version = "1.3.0")
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node", dev_dependency = True)
@@ -62,10 +63,25 @@ new_git_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:git.bzl"
new_git_repository(
name = "tinyobjloader",
branch = "release",
commit = "3bb554cf74428d7db13418b4aca1b9752a1d2be8",
remote = "https://github.com/tinyobjloader/tinyobjloader.git",
)
swift_deps = use_extension("@rules_swift_package_manager//:extensions.bzl", "swift_deps")
swift_deps.from_package(
resolved = "platform/ios/app-swift/Package.resolved",
swift = "platform/ios/app-swift/Package.swift",
)
# https://github.com/cgrindel/rules_swift_package_manager?tab=readme-ov-file#quickstart
# This is generated by the ruleset. Do not edit
use_repo(
swift_deps,
"swift_package",
"swiftpkg_polyline",
"swiftpkg_sentry_cocoa",
)
darwin_config = use_repo_rule("//platform/darwin:bazel/darwin_config_repository_rule.bzl", "darwin_config")
darwin_config(

View File

@@ -1,35 +1,52 @@
load("@darwin_config//:config.bzl", "BUNDLE_ID_PREFIX")
load("@rules_apple//apple:ios.bzl", "ios_application")
load(
"@rules_swift//swift:swift.bzl",
"swift_library",
)
load("@rules_apple//apple:resources.bzl", "apple_resource_group")
load("@rules_swift//swift:swift.bzl", "swift_library")
load("//platform/ios/bazel:macros.bzl", "env_info_plist")
load("//platform/ios/bazel:provisioning.bzl", "configure_device_profiles")
configure_device_profiles()
apple_resource_group(
name = "NavigationRoutes",
strip_structured_resources_prefixes = ["Data"],
structured_resources = ["Data/Routes"],
visibility = ["//visibility:public"],
)
# add SENTRY_DSN from the environment to the plist
env_info_plist(
name = "info_swift_plist",
input = "Info.plist",
output = "Info-swift.plist",
vars = ["SENTRY_DSN"],
)
swift_library(
name = "Sources",
srcs = glob(["Sources/*.swift"]),
data = glob([
"Data/*",
srcs = glob(["Sources/**/*.swift"]),
data = [":NavigationRoutes"] + glob([
"Data/*.geojson",
"Data/*.png",
"Assets.xcassets/*",
]),
module_name = "Sources",
tags = ["manual"],
deps = [
"//platform:ios-sdk",
"@swiftpkg_polyline//:Polyline",
"@swiftpkg_sentry_cocoa//:Sentry",
],
)
ios_application(
name = "MapLibreApp",
bundle_id = "{}.swiftapp".format(BUNDLE_ID_PREFIX),
bundle_id = "{}.maplibre.app".format(BUNDLE_ID_PREFIX),
families = [
"iphone",
"ipad",
],
infoplists = [":Info.plist"],
infoplists = [":Info-swift.plist"],
minimum_os_version = "16.0",
provisioning_profile = "xcode_profile",
resources = [

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"pins" : [
{
"identity" : "polyline",
"kind" : "remoteSourceControl",
"location" : "https://github.com/raphaelmor/Polyline",
"state" : {
"revision" : "353f80378dcd8f17eefe8550090c6b1ae3c9da23",
"version" : "5.1.0"
}
},
{
"identity" : "sentry-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa",
"state" : {
"revision" : "8627bbc0e663238d516b541d8fc7edd62a627fde",
"version" : "8.57.3"
}
}
],
"version" : 2
}

View File

@@ -0,0 +1,16 @@
// swift-tools-version: 5.9
import PackageDescription
// dummy package to fetch dependencies via bazel `rules_swift_package_manager``
// https://github.com/cgrindel/rules_swift_package_manager?tab=readme-ov-file#4-run-swift-package-update
let package = Package(
name: "maplibre-swift",
defaultLocalization: "en",
platforms: [.iOS(.v12)],
dependencies: [
.package(url: "https://github.com/raphaelmor/Polyline", from: "5.1.0"),
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.54.0"),
]
)

View File

@@ -1,9 +1,22 @@
import MapLibre
import Sentry
import SwiftUI
import UIKit
@main
struct MapLibreApp: App {
init() {
if let dsn = Bundle.main.object(forInfoDictionaryKey: "SENTRY_DSN") as? String {
SentrySDK.start { options in
options.dsn = dsn
options.enableCrashHandler = true
options.enabled = true
}
print("Sentry initialized")
}
}
var body: some Scene {
WindowGroup {
MapLibreNavigationView()

View File

@@ -40,6 +40,9 @@ struct MapLibreNavigationView: View {
NavigationLink("ObserverExample") {
ObserverExampleViewUIViewControllerRepresentable()
}
NavigationLink("LongRunningMap") {
LongRunningMapView().edgesIgnoringSafeArea(.all)
}
Group {
NavigationLink("AnimatedLineExample") {
AnimatedLineExampleUIViewControllerRepresentable()

View File

@@ -0,0 +1,47 @@
import MapLibre
import SwiftUI
import UIKit
struct UserMapView: UIViewRepresentable {
func makeUIView(context _: Context) -> UIView {
let map = UserMap()
map.run()
return map
}
func updateUIView(_: UIView, context _: Context) {}
}
struct NavigationMapView: UIViewRepresentable {
func makeUIView(context _: Context) -> UIView {
let map = NavigationMap()
map.run()
return map
}
func updateUIView(_: UIView, context _: Context) {}
}
struct LongRunningMapView: View {
// view lifetime (seconds)
let DURATION = 72.0 * 60.0 * 60.0
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
UserMapView()
NavigationMapView()
}
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
}
.onReceive(Timer.publish(every: DURATION, on: .current, in: .default).autoconnect()) { _ in
printMemoryUsage()
dismiss()
}
}
}

View File

@@ -0,0 +1,215 @@
import MapLibre
import Polyline
@objc public protocol NavigationLocationManagerDelegate: AnyObject {
@objc func progressDidChange(currentDistance: Double, remainingDistance: Double)
@objc func navigationDidComplete()
}
class NavigationLocationManager: NSObject, MLNLocationManager {
var delegate: (any MLNLocationManagerDelegate)?
var authorizationStatus: CLAuthorizationStatus = .authorizedAlways
var headingOrientation: CLDeviceOrientation = .portrait
var navigationDelegate: (any NavigationLocationManagerDelegate)?
var updateInterval = 0.2
var speed = 20.0
var speedMultiplier = 1.0
var currentDistance = 0.0
var totalDistance = 0.0
var coordinates: [CLLocationCoordinate2D] = []
init(coordinates: [CLLocationCoordinate2D]) {
super.init()
self.coordinates = coordinates
totalDistance = zip(coordinates, coordinates.dropFirst())
.map { $0.distance(to: $1) }
.reduce(0.0, +)
}
func requestAlwaysAuthorization() {}
func requestWhenInUseAuthorization() {}
func dismissHeadingCalibrationDisplay() {}
func startUpdatingHeading() {}
func stopUpdatingHeading() {}
func startUpdatingLocation() {
DispatchQueue.main.async(execute: update)
}
func stopUpdatingLocation() {
DispatchQueue.main.async {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.update), object: nil)
}
}
@objc private func update() {
guard coordinates.count > 0 else { return }
let currentCoord = getCoord(distance: currentDistance)
let nextCoord = getCoord(distance: currentDistance + 10)
let location = CLLocation(coordinate: currentCoord,
altitude: 0.0,
horizontalAccuracy: 40.0,
verticalAccuracy: 10.0,
course: currentCoord.direction(to: nextCoord).wrap(min: 0, max: 360),
speed: speed,
timestamp: Date())
delegate?.locationManager(self, didUpdate: [location])
navigationDelegate?.progressDidChange(currentDistance: currentDistance, remainingDistance: max(totalDistance - currentDistance, 0.0))
if currentCoord.latitude == coordinates.last?.latitude,
currentCoord.longitude == coordinates.last?.longitude
{
navigationDelegate?.navigationDidComplete()
return
}
perform(#selector(update), with: nil, afterDelay: updateInterval)
// can get a more dynamic speed value from the route steps
currentDistance += speed * speedMultiplier * updateInterval
}
func getCoord(distance: Double) -> CLLocationCoordinate2D {
var traveled = 0.0
for i in 0 ..< coordinates.count {
guard distance < traveled || i < coordinates.count - 1 else {
break
}
if traveled >= distance {
let overshoot = distance - traveled
if overshoot == 0 {
return coordinates[i]
}
let direction = coordinates[i].direction(to: coordinates[i - 1]) - 180
return coordinates[i].coordinate(at: overshoot, facing: direction)
}
traveled += coordinates[i].distance(to: coordinates[i + 1])
}
return coordinates.last!
}
}
class NavigationRouteStep {
var name: String?
var duration = 0.0
var distance = 0.0
var maneuverType: String?
var maneuverBearingBefore = 0
var maneuverBearingAfter = 0
var location = CLLocationCoordinate2D()
init(json: [String: Any]) {
name = json["name"] as? String
duration = json["duration"] as! Double
distance = json["distance"] as! Double
let maneuverJson = json["maneuver"] as! [String: Any]
maneuverType = maneuverJson["type"] as? String
maneuverBearingBefore = maneuverJson["bearing_before"] as! Int
maneuverBearingAfter = maneuverJson["bearing_after"] as! Int
let locationJson = maneuverJson["location"] as! [Double]
location = CLLocationCoordinate2D(latitude: locationJson.last!, longitude: locationJson.first!)
}
}
class NavigationRoute {
let SourceIdentifier = "routeSource"
let LayerIdentifier = "routeLayer"
let LineWidthByZoomLevel: [Int: Double] = [
10: 12,
13: 13.5,
16: 16.5,
19: 33,
22: 42,
]
var mapView: MLNMapView?
var distance = 0.0
var duration = 0.0
var destination: CLLocationCoordinate2D?
var steps: [NavigationRouteStep]?
init(json: [String: Any], mapView: MLNMapView) {
self.mapView = mapView
load(json: json)
}
deinit {
mapView?.showsUserLocation = false
mapView?.locationManager = nil
}
private func load(json: [String: Any]) {
let routeJson = (json["routes"] as! [[String: Any]]).first!
// stats
distance = routeJson["distance"] as! Double
duration = routeJson["duration"] as! Double
let waypointsJson = json["waypoints"] as! [[String: Any]]
let destinationJson = waypointsJson.last!["location"] as! [Double]
destination = CLLocationCoordinate2D(latitude: destinationJson.last!, longitude: destinationJson.first!)
// route geometry
let geometry: [LocationCoordinate2D] = decodePolyline(routeJson["geometry"] as! String, precision: 1e6)!
loadGeometry(from: geometry)
let locationManager = NavigationLocationManager(coordinates: geometry)
locationManager.speed = distance / duration
mapView?.locationManager = locationManager
// route steps
let legsJson = routeJson["legs"] as! [[String: Any]]
let stepsJson = legsJson.first!["steps"] as! [[String: Any]]
steps = stepsJson.map { NavigationRouteStep(json: $0) }
}
private func loadGeometry(from geometry: [CLLocationCoordinate2D]) {
let polylines = MLNShapeCollectionFeature(shapes: [MLNPolylineFeature(coordinates: geometry, count: UInt(geometry.count))])
let source = MLNShapeSource(identifier: SourceIdentifier, shape: polylines, options: nil)
mapView?.style?.addSource(source)
let layer = MLNLineStyleLayer(identifier: LayerIdentifier, source: source)
layer.lineWidth = NSExpression(forMLNInterpolating: .zoomLevelVariable,
curveType: .linear,
parameters: nil,
stops: NSExpression(forConstantValue: LineWidthByZoomLevel))
layer.lineColor = NSExpression(forConstantValue: UIColor.blue)
layer.lineOpacity = NSExpression(forConstantValue: 1)
layer.lineCap = NSExpression(forConstantValue: "round")
layer.lineJoin = NSExpression(forConstantValue: "round")
for value in mapView!.style!.layers.reversed() {
if !(value is MLNSymbolStyleLayer) {
mapView?.style?.insertLayer(layer, below: value)
break
}
}
// destination waypoint annotation
let annotation = MLNPointAnnotation()
annotation.coordinate = destination!
mapView?.addAnnotation(annotation)
}
func start() {
mapView?.userTrackingMode = .followWithCourse
mapView?.showsUserLocation = true
}
}

View File

@@ -0,0 +1,135 @@
import MapLibre
private class NavigationConfig {
var RANDOM = RandomNumberGeneratorWithSeed(seed: 42)
let STYLES = [
AMERICANA_STYLE,
OPENFREEMAP_LIBERTY_STYLE,
OPENFREEMAP_BRIGHT_STYLE,
AWS_OPEN_DATA_STANDARD_LIGHT_STYLE,
PROTOMAPS_LIGHT_STYLE,
PROTOMAPS_DARK_STYLE,
PROTOMAPS_GRAYSCALE_STYLE,
PROTOMAPS_WHITE_STYLE,
PROTOMAPS_BLACK_STYLE,
]
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 randomAltitude() -> Double {
random(in: 800.0 ... 2000.0)
}
func randomTilt() -> Double {
random(in: 30.0 ... 60.0)
}
func randomWaitTime() -> TimeInterval {
random(in: 5000 ... 10000) / 1000.0
}
func randomSpeed() -> Double {
random(in: 0.7 ... 2.0)
}
func getRouteResponseJson() -> [String: Any]? {
var responseJson: [String: Any]?
do {
let routeDirectory = Bundle.main.resourcePath! + "/Routes/"
let routeFile = try FileManager.default.contentsOfDirectory(atPath: routeDirectory).randomElement(using: &RANDOM)!
let jsonStr = try String(contentsOfFile: routeDirectory + routeFile, encoding: .utf8)
responseJson = try JSONSerialization.jsonObject(with: jsonStr.data(using: .utf8)!, options: []) as? [String: Any] ?? [:]
} catch {
print("NavigationMap: \(error)")
}
return responseJson
}
}
class NavigationMap: MLNMapView, MLNMapViewDelegate, NavigationLocationManagerDelegate {
fileprivate let config = NavigationConfig()
private var route: NavigationRoute?
private var continuation: CheckedContinuation<Void, Never>?
init() {
super.init(frame: CGRect())
}
@available(*, unavailable)
@MainActor required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@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
}
}
@objc(mapView:didFinishLoadingStyle:) func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) {
continuation?.resume()
continuation = nil
}
func run() {
delegate = self
startNewRoute()
}
func startNewRoute() {
Task {
try await Task.sleep(for: .seconds(config.randomWaitTime()))
// reset existing route
route = nil
await load(style: config.STYLES.randomElement(using: &config.RANDOM)!!)
let routeJson = config.getRouteResponseJson()
route = NavigationRoute(json: routeJson!, mapView: self)
let locationManager = locationManager as! NavigationLocationManager
let startingWaypoint = locationManager.getCoord(distance: 0.0)
locationManager.navigationDelegate = self
locationManager.speedMultiplier = config.randomSpeed()
let camera = MLNMapCamera(
lookingAtCenter: startingWaypoint,
altitude: config.randomAltitude(),
pitch: config.randomTilt(),
heading: 0.0
)
setCamera(camera, animated: true)
try await Task.sleep(for: .seconds(config.randomWaitTime()))
route!.start()
}
}
func progressDidChange(currentDistance _: Double, remainingDistance _: Double) {}
func navigationDidComplete() {
startNewRoute()
}
}

View File

@@ -0,0 +1,59 @@
import CoreLocation
public extension CLLocationDegrees {
func toRadians() -> CLLocationDegrees {
self * .pi / 180.0
}
func toDegrees() -> CLLocationDegrees {
self * 180.0 / .pi
}
}
public extension CLLocationCoordinate2D {
func distance(to coordinate: CLLocationCoordinate2D) -> CLLocationDistance {
let metersPerRadian = 6_373_000.0
let A = toRadians()
let B = coordinate.toRadians()
let a = pow(sin((B.latitude - A.latitude) / 2), 2)
+ pow(sin((B.longitude - A.longitude) / 2), 2) * cos(A.latitude) * cos(B.latitude)
return 2 * atan2(sqrt(a), sqrt(1 - a)) * metersPerRadian
}
func direction(to coordinate: CLLocationCoordinate2D) -> CLLocationDegrees {
let A = toRadians()
let B = coordinate.toRadians()
let a = sin(B.longitude - A.longitude) * cos(B.latitude)
let b = cos(A.latitude) * sin(B.latitude) - sin(A.latitude) * cos(B.latitude) * cos(B.longitude - A.longitude)
return Measurement(value: atan2(a, b), unit: UnitAngle.radians).converted(to: .degrees).value
}
func coordinate(at distance: CLLocationDistance, facing direction: CLLocationDirection) -> CLLocationCoordinate2D {
let metersPerRadian = 6_373_000.0
let distance = distance / metersPerRadian
let direction = Measurement(value: direction, unit: UnitAngle.degrees).converted(to: .radians).value
let coordRadians = toRadians()
let otherLatitude = asin(sin(coordRadians.latitude) * cos(distance) + cos(coordRadians.latitude) * sin(distance) * cos(direction))
let otherLongitude = coordRadians.longitude + atan2(sin(direction) * sin(distance) * cos(coordRadians.latitude),
cos(distance) - sin(coordRadians.latitude) * sin(otherLatitude))
return CLLocationCoordinate2D(latitude: otherLatitude, longitude: otherLongitude).toDegrees()
}
func toRadians() -> CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: latitude.toRadians(), longitude: longitude.toRadians())
}
func toDegrees() -> CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: latitude.toDegrees(), longitude: longitude.toDegrees())
}
}
public extension CLLocationDirection {
func wrap(min minimumValue: CLLocationDirection, max maximumValue: CLLocationDirection) -> CLLocationDirection {
let d = maximumValue - minimumValue
return fmod(fmod(self - minimumValue, d) + d, d) + minimumValue
}
}

View File

@@ -0,0 +1,277 @@
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,
AWS_OPEN_DATA_STANDARD_LIGHT_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)
}
}
}

View File

@@ -0,0 +1,68 @@
import MapLibre
import MachO
struct RandomNumberGeneratorWithSeed: RandomNumberGenerator {
private var state1: UInt64
private var state2: UInt64
init(seed: UInt64) {
state1 = seed
state2 = seed + 1
}
mutating func next() -> UInt64 {
let s1 = state1
let s2 = state2
state1 = s2
state2 = s1 ^ (s1 << 23)
state2 ^= s2 ^ (s2 >> 17)
state2 ^= s1 ^ (s1 >> 26)
return state1 &+ state2
}
}
extension Int {
@MainActor func `repeat`(f: () async -> Void) async {
for _ in 0 ..< self {
await f()
}
}
func `repeat`(f: () -> Void) {
for _ in 0 ..< self {
f()
}
}
}
extension MLNMapView {
@MainActor func animate(camera: MLNMapCamera, withDuration duration: TimeInterval) async {
await withCheckedContinuation { continuation in
self.setCamera(camera, withDuration: duration, animationTimingFunction: nil) {
continuation.resume()
}
}
}
}
func getMemoryUsage() -> UInt64 {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let result: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
guard result == KERN_SUCCESS else { return 0 }
return UInt64(info.resident_size)
}
func printMemoryUsage() {
print("Total memory: \(Float(ProcessInfo.processInfo.physicalMemory) / 1024 / 1024) MB")
print("Used memory: \(getMemoryUsage() / 1024 / 1024) MB")
}

View File

@@ -2,9 +2,22 @@ import Foundation
// #-example-code(ExampleStyles)
let AMERICANA_STYLE = URL(string:
"https://americanamap.org/style.json")
let DEMOTILES_STYLE = URL(string: "https://demotiles.maplibre.org/style.json")
let AMERICANA_STYLE = URL(string: "https://americanamap.org/style.json")
let OPENFREEMAP_LIBERTY_STYLE = URL(string: "https://tiles.openfreemap.org/styles/liberty")
let OPENFREEMAP_BRIGHT_STYLE = URL(string: "https://tiles.openfreemap.org/styles/bright")
let VERSATILES_COLORFUL_STYLE = URL(string: "https://tiles.versatiles.org/assets/styles/colorful.json")
// #-end-example-code
let AWS_OPEN_DATA_STANDARD_LIGHT_STYLE = URL(string: "https://maps.geo.us-east-2.amazonaws.com/maps/v0/maps/OpenDataStyle/style-descriptor?key=v1.public.eyJqdGkiOiI1NjY5ZTU4My0yNWQwLTQ5MjctODhkMS03OGUxOTY4Y2RhMzgifR_7GLT66TNRXhZJ4KyJ-GK1TPYD9DaWuc5o6YyVmlikVwMaLvEs_iqkCIydspe_vjmgUVsIQstkGoInXV_nd5CcmqRMMa-_wb66SxDdbeRDvmmkpy2Ow_LX9GJDgL2bbiCws0wupJPFDwWCWFLwpK9ICmzGvNcrPbX5uczOQL0N8V9iUvziA52a1WWkZucIf6MUViFRf3XoFkyAT15Ll0NDypAzY63Bnj8_zS8bOaCvJaQqcXM9lrbTusy8Ftq8cEbbK5aMFapXRjug7qcrzUiQ5sr0g23qdMvnKJQFfo7JuQn8vwAksxrQm6A0ByceEXSfyaBoVpFcTzEclxUomhY.NjAyMWJkZWUtMGMyOS00NmRkLThjZTMtODEyOTkzZTUyMTBi")
private func protomaps(_ style: String) -> URL? {
URL(string: "https://api.protomaps.com/styles/v2/\(style).json?key=e761cc7daedf832a")
}
let PROTOMAPS_LIGHT_STYLE = protomaps("light")
let PROTOMAPS_DARK_STYLE = protomaps("dark")
let PROTOMAPS_GRAYSCALE_STYLE = protomaps("grayscale")
let PROTOMAPS_WHITE_STYLE = protomaps("white")
let PROTOMAPS_BLACK_STYLE = protomaps("black")

View File

@@ -17,3 +17,49 @@ def info_plist(name, base_info_plist, out, **kwargs):
""").format(base_info_plist),
**kwargs
)
def _env_info_plist_impl(ctx):
args = ctx.actions.args()
args.add(ctx.file.input)
args.add(ctx.outputs.output)
args.add_all(ctx.attr.vars)
ctx.actions.run_shell(
inputs = [ctx.file.input],
outputs = [ctx.outputs.output],
arguments = [args],
use_default_shell_env = True,
command = """
in=$1
out=$2
shift 2
vars=(${@})
cp $in $out
for var in "${vars[@]}"; do
if [ -n "${!var}" ]; then
if plutil -p $out | grep -q "<key>$var<key>"; then
# skip if there is a key present (let the user manually override)
#plutil -replace $var -string ${!var} $out
echo "User $var key detected. Update ignored"
else
plutil -insert $var -string ${!var} $out
fi
fi
done
""",
)
env_info_plist = rule(
implementation = _env_info_plist_impl,
attrs = {
"input": attr.label(
mandatory = True,
allow_single_file = True,
),
"output": attr.output(mandatory = True),
"vars": attr.string_list(mandatory = True),
},
doc = "Pull environment variables and add them to the provided plist",
)