iOS long running test v2 (#3978)
Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
2
.github/workflows/linux-ci.yml
vendored
2
.github/workflows/linux-ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
18
MODULE.bazel
18
MODULE.bazel
@@ -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(
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
7018
platform/ios/app-swift/Data/Routes/route_BER_15453.json
Normal file
7018
platform/ios/app-swift/Data/Routes/route_BER_15453.json
Normal file
File diff suppressed because it is too large
Load Diff
2380
platform/ios/app-swift/Data/Routes/route_BER_3507.json
Normal file
2380
platform/ios/app-swift/Data/Routes/route_BER_3507.json
Normal file
File diff suppressed because it is too large
Load Diff
5011
platform/ios/app-swift/Data/Routes/route_DC_40743.json
Normal file
5011
platform/ios/app-swift/Data/Routes/route_DC_40743.json
Normal file
File diff suppressed because it is too large
Load Diff
2438
platform/ios/app-swift/Data/Routes/route_DC_4453.json
Normal file
2438
platform/ios/app-swift/Data/Routes/route_DC_4453.json
Normal file
File diff suppressed because it is too large
Load Diff
4318
platform/ios/app-swift/Data/Routes/route_SF_20753.json
Normal file
4318
platform/ios/app-swift/Data/Routes/route_SF_20753.json
Normal file
File diff suppressed because it is too large
Load Diff
1220
platform/ios/app-swift/Data/Routes/route_SF_2122.json
Normal file
1220
platform/ios/app-swift/Data/Routes/route_SF_2122.json
Normal file
File diff suppressed because it is too large
Load Diff
23
platform/ios/app-swift/Package.resolved
Normal file
23
platform/ios/app-swift/Package.resolved
Normal 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
|
||||
}
|
||||
16
platform/ios/app-swift/Package.swift
Normal file
16
platform/ios/app-swift/Package.swift
Normal 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"),
|
||||
]
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -40,6 +40,9 @@ struct MapLibreNavigationView: View {
|
||||
NavigationLink("ObserverExample") {
|
||||
ObserverExampleViewUIViewControllerRepresentable()
|
||||
}
|
||||
NavigationLink("LongRunningMap") {
|
||||
LongRunningMapView().edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
Group {
|
||||
NavigationLink("AnimatedLineExample") {
|
||||
AnimatedLineExampleUIViewControllerRepresentable()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
215
platform/ios/app-swift/Sources/Stability/Navigation.swift
Normal file
215
platform/ios/app-swift/Sources/Stability/Navigation.swift
Normal 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
|
||||
}
|
||||
}
|
||||
135
platform/ios/app-swift/Sources/Stability/NavigationMap.swift
Normal file
135
platform/ios/app-swift/Sources/Stability/NavigationMap.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
277
platform/ios/app-swift/Sources/Stability/UserMap.swift
Normal file
277
platform/ios/app-swift/Sources/Stability/UserMap.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
68
platform/ios/app-swift/Sources/Stability/Utils.swift
Normal file
68
platform/ios/app-swift/Sources/Stability/Utils.swift
Normal 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")
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user