diff --git a/CTilePng.podspec b/CTilePng.podspec new file mode 100644 index 0000000..f9ffcd9 --- /dev/null +++ b/CTilePng.podspec @@ -0,0 +1,20 @@ +Pod::Spec.new do |s| + s.name = "CTilePng" + s.version = "1.3.1" + s.summary = "The C shim MapConductorCore's Rust PNG encoder is reached through." + s.license = { :type => "Apache-2.0", :file => "LICENSE" } + s.author = "MapConductor" + s.homepage = "https://github.com/MapConductor/ios-sdk-core" + s.source = { :git => "https://github.com/MapConductor/ios-sdk-core.git", :tag => s.version.to_s } + s.platform = :ios, "15.1" + # Its own pod, not source files inside MapConductorCore. + # + # `TilePngEncoder.swift` says `import CTilePng`, which needs CTilePng to be + # a module. Compiled *into* MapConductorCore the C has no module of its own + # and that import does not resolve -- Swift Package Manager gives it one + # because it is a separate target there, and CocoaPods only does the same if + # it is a separate pod. Without this the pod builds everywhere except where + # it matters: React Native, which consumes the source pod. + s.source_files = "Sources/CTilePng/**/*.{c,h}" + s.public_header_files = "Sources/CTilePng/include/*.h" +end diff --git a/MapConductorCore.podspec b/MapConductorCore.podspec index 50203a2..6633088 100644 --- a/MapConductorCore.podspec +++ b/MapConductorCore.podspec @@ -9,4 +9,11 @@ Pod::Spec.new do |s| s.platform = :ios, "15.1" s.swift_version = "5.9" s.source_files = "Sources/MapConductorCore/**/*.swift" + # The C shim is its own pod so that `import CTilePng` resolves. See + # CTilePng.podspec for why it cannot be source files in here. + s.dependency "CTilePng" + # The Rust PNG encoder. Marker tiles are rasterised in-process and the + # platform encoder is several times slower; MarkerTileRenderer falls back to + # it if this is missing, so the pod stays usable either way. + s.vendored_frameworks = "TilePng.xcframework" end diff --git a/Package.swift b/Package.swift index c65434c..ab30fd1 100644 --- a/Package.swift +++ b/Package.swift @@ -28,8 +28,18 @@ let package = Package( targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. + // The Rust PNG encoder. Built and packaged by the + // mapconductor-vectortile repo (scripts/build-ios-core-png.sh) and + // copied here by its scripts/sync-sdk.sh. + // The Rust PNG encoder. The archive and its header travel separately: + // Xcode merges every linked XCFramework's Headers into one include + // directory, so two of them shipping a module.modulemap collide, which + // anything depending on both this and MvtRender would hit. + .binaryTarget(name: "TilePng", path: "TilePng.xcframework"), + .target(name: "CTilePng"), .target( - name: "MapConductorCore" + name: "MapConductorCore", + dependencies: ["CTilePng", "TilePng"] ), .testTarget( name: "MapConductorCoreTests", diff --git a/Sources/CTilePng/include/tile_png.h b/Sources/CTilePng/include/tile_png.h new file mode 100644 index 0000000..efd0580 --- /dev/null +++ b/Sources/CTilePng/include/tile_png.h @@ -0,0 +1,40 @@ +// PNG encoding for the iOS SDK core. Mirrors crates/tile-png-ffi. +// +// Travels as a source header in ios-sdk-core rather than inside the +// XCFramework: Xcode merges every linked framework's Headers into one include +// directory, and two frameworks shipping a module.modulemap collide there. +#ifndef TILE_PNG_H +#define TILE_PNG_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Swift's C importer keeps a bare 0 but drops macros whose body starts with a +// minus, so the error codes are restated on the Swift side. Keep them in step. +#define TILE_PNG_OK 0 +#define TILE_PNG_ERR_BAD_ARGUMENT (-1) +#define TILE_PNG_ERR_ENCODE_FAILED (-2) +#define TILE_PNG_ERR_PANIC (-3) + +/// Encodes an RGBA8 buffer as PNG. Release the result with +/// tile_png_buffer_free. When `premultiplied` is non-zero the input buffer is +/// un-premultiplied in place and must not be reused. +int32_t tile_png_encode(uint8_t *rgba, + uint32_t width, + uint32_t height, + int32_t premultiplied, + uint8_t **out_ptr, + size_t *out_len); + +/// Releases a buffer returned by tile_png_encode. +void tile_png_buffer_free(uint8_t *ptr, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* TILE_PNG_H */ diff --git a/Sources/CTilePng/shim.c b/Sources/CTilePng/shim.c new file mode 100644 index 0000000..4f34c53 --- /dev/null +++ b/Sources/CTilePng/shim.c @@ -0,0 +1,3 @@ +// SwiftPM needs a target to have at least one source file. This one exists so +// CTilePng can be a target whose only real content is its public header; the +// implementation is linked from the TilePng XCFramework. diff --git a/Sources/MapConductorCore/LocalTileServer.swift b/Sources/MapConductorCore/LocalTileServer.swift index f6067d2..e553174 100644 --- a/Sources/MapConductorCore/LocalTileServer.swift +++ b/Sources/MapConductorCore/LocalTileServer.swift @@ -1,24 +1,70 @@ import Foundation import Network -public final class LocalTileServer { +public final class LocalTileServer: @unchecked Sendable { public private(set) var baseUrl: String private let listener: NWListener private let queue: DispatchQueue private let providersLock = NSLock() private var providers: [String: TileProvider] = [:] + /// Guarded by `providersLock`. + private var documents: [String: Document] = [:] + /// Guarded by `providersLock`. Directories served by route: an offline + /// package a map reads its tiles, glyphs and sprite from. + private var fileRoutes: [String: FileRoute] = [:] private let cacheOptionsLock = NSLock() private var forceNoStoreCache: Bool private let connectionsLock = NSLock() private var activeConnections = 0 private var shedConnections = 0 + /** + The lane tile drawing runs in: at most `renderWidth` at a time, below the + UI in priority. + + Without it, every connection rendered its tile on the shared concurrent + queue, synchronously, as it arrived. A pinch makes the map ask for tiles at + every zoom the gesture passes through, so dozens of renders ran at once; + GCD answers that by spawning threads — far past the core count — and the + gesture itself starves. That is what "the map computes while I pinch" feels + like. android-sdk never had this: its server has had a bounded pool + (`MAX_WORKER_THREADS = 8`) from the start, which is why the same pinch on a + Pixel 5a feels fine. + + `.utility` matters as much as the width. The renders are pure CPU; at + default QoS they compete with the main thread on equal terms, at utility + the gesture preempts them. + */ + private let renderQueue = DispatchQueue( + label: "MapConductorCore.LocalTileServer.render", + qos: .utility, + attributes: .concurrent + ) + private let renderGate: DispatchSemaphore + private let renderWidth: Int + + /// Counters behind the once-a-second summary log. + private let statsLock = NSLock() + private var statsWindowStart = DispatchTime.now().uptimeNanoseconds + private var statsRendered = 0 + private var statsRenderNanos: UInt64 = 0 + private var statsMaxWaitNanos: UInt64 = 0 + private var statsMaxActive = 0 + private var statsNotFound = 0 + private var statsFailed = 0 + private var statsAbandoned = 0 + private var activeRenders = 0 + private init(listener: NWListener, queue: DispatchQueue, baseUrl: String, forceNoStoreCache: Bool) { self.listener = listener self.queue = queue self.baseUrl = baseUrl self.forceNoStoreCache = forceNoStoreCache + // Two cores stay free for the gesture and the map's own GL thread. + // The same width android-sdk converges on for its 8-core devices. + renderWidth = min(8, max(2, ProcessInfo.processInfo.activeProcessorCount - 2)) + renderGate = DispatchSemaphore(value: renderWidth) } public func register(routeId: String, provider: TileProvider) { @@ -33,6 +79,64 @@ public final class LocalTileServer { providersLock.unlock() } + /** + Serves `body` at ``documentUrl(id:)`` until ``unregisterDocument(id:)``. + + Whole documents beside the tiles: a style JSON that a vector-capable map + is to read directly, for one. Served `no-store` -- the id changes when the + content does, so there is nothing to revalidate, and a map holding on to a + stale document is harder to notice than one refetching a small one. + + `id` must be URL-safe as given; it is matched against the request path + verbatim. + */ + public func registerDocument(id: String, contentType: String, body: Data) { + providersLock.lock() + documents[id] = Document(contentType: contentType, body: body) + providersLock.unlock() + } + + public func unregisterDocument(id: String) { + providersLock.lock() + documents.removeValue(forKey: id) + providersLock.unlock() + } + + public func documentUrl(id: String) -> String { + "\(baseUrl)/\(Self.documentsPrefix)/\(id)" + } + + /** + Serves the files under `directory` at ``filesUrl(routeId:)```/`. + + The content type follows the extension (`.mvt`, `.pbf`, `.json`, `.png`); + a file is served as immutable, since a package does not change under a + map. A path with no file is answered by `fallback` -- served `no-store`, + so what came from upstream is not mistaken for the package -- or, when + that is nil or answers nil, with 404, which the map takes as "no such + tile" and does not retry. The fallback runs on the render queue, off the + connection's thread. + */ + public func registerFiles( + routeId: String, + directory: URL, + fallback: (@Sendable (_ relativePath: String) -> Data?)? = nil + ) { + providersLock.lock() + fileRoutes[routeId] = FileRoute(directory: directory, fallback: fallback) + providersLock.unlock() + } + + public func unregisterFiles(routeId: String) { + providersLock.lock() + fileRoutes.removeValue(forKey: routeId) + providersLock.unlock() + } + + public func filesUrl(routeId: String) -> String { + "\(baseUrl)/\(Self.filesPrefix)/\(routeId)" + } + public var isListening: Bool { listener.state == .ready } @@ -57,6 +161,59 @@ public final class LocalTileServer { "\(baseUrl)/tiles/\(routeId)/\(tileSize)/\(cacheKey)/{z}/{x}/{y}.png" } + /** + Renders one of this server's URLs without going through TCP. + + Native map SDKs that expose an in-process tile callback should use this + path. Besides avoiding a loopback HTTP round trip, their task cancellation + is authoritative: unlike a TCP FIN it really does mean that the map no + longer wants the tile. + + The same render gate is shared with HTTP clients, so using the direct path + cannot create an unbounded number of CPU-heavy renders. + */ + public func renderLocalTile( + url: URL, + isCancelled: @escaping @Sendable () -> Bool = { false } + ) -> Data? { + guard url.absoluteString.hasPrefix(baseUrl + "/") else { return nil } + + let queuedAt = DispatchTime.now().uptimeNanoseconds + while renderGate.wait(timeout: .now() + .milliseconds(10)) != .success { + if isCancelled() { return nil } + } + let waitNanos = DispatchTime.now().uptimeNanoseconds - queuedAt + + statsLock.lock() + activeRenders += 1 + statsMaxActive = max(statsMaxActive, activeRenders) + statsLock.unlock() + + let renderStart = DispatchTime.now().uptimeNanoseconds + let outcome = resolveTile( + path: url.path, sourceZoomOffset: Self.sourceZoomOffset(in: url.absoluteString), + isCancelled: isCancelled + ) + + let renderNanos = DispatchTime.now().uptimeNanoseconds - renderStart + + statsLock.lock() + activeRenders -= 1 + statsLock.unlock() + renderGate.signal() + + record(outcome: outcome, path: url.path, waitNanos: waitNanos, renderNanos: renderNanos) + + switch outcome { + case .tile(let response): + return response.body + case .empty(let pixelSize, _): + return TransparentTile.png(size: pixelSize) + case .notFound, .failed, .abandoned: + return nil + } + } + @available(*, deprecated, message: "`version` is ignored. Use `urlTemplate(routeId:tileSize:)` instead.") public func urlTemplate(routeId: String, version: Int64) -> String { urlTemplate(routeId: routeId, tileSize: RasterLayerSource.defaultTileSize) @@ -121,7 +278,7 @@ public final class LocalTileServer { // itself stays on the wildcard address (dual-stack) because some map // SDK HTTP stacks resolve localhost to ::1. guard isLoopback(connection) else { - MCLog.marker("LocalTileServer: rejected non-loopback connection from \(connection.endpoint)") + MCLog.tileServer("LocalTileServer: rejected non-loopback connection from \(connection.endpoint)") connection.cancel() return } @@ -132,7 +289,7 @@ public final class LocalTileServer { shedConnections += 1 let shed = shedConnections connectionsLock.unlock() - MCLog.marker("LocalTileServer: shed connection (saturated) total=\(shed)") + MCLog.tileServer("LocalTileServer: shed connection (saturated) total=\(shed)") connection.cancel() return } @@ -234,8 +391,250 @@ public final class LocalTileServer { return } - let path = request.path.split(separator: "?")[0] - let tileResponse = resolveTile(path: String(path)) + let path = String(request.path.split(separator: "?")[0]) + let sourceZoomOffset = Self.sourceZoomOffset(in: request.path) + + let filesPrefix = "/\(Self.filesPrefix)/" + if path.hasPrefix(filesPrefix) { + serveFile( + routePath: String(path.dropFirst(filesPrefix.count)), + connection: connection, + remainingData: remainingData, + keepAlive: keepAlive, + handled: handled + ) + return + } + + let documentPrefix = "/\(Self.documentsPrefix)/" + if path.hasPrefix(documentPrefix) { + let id = String(path.dropFirst(documentPrefix.count)) + providersLock.lock() + let document = documents[id] + providersLock.unlock() + sendResponse( + connection: connection, + status: document == nil ? "404 Not Found" : "200 OK", + contentType: document?.contentType ?? "text/plain", + body: document?.body ?? Data("Not found".utf8), + keepAlive: keepAlive, + extraHeaders: ["Cache-Control": Self.noStoreCacheControl] + ) { [weak self, weak connection] in + guard let self, let connection else { return } + self.finishRequest(connection: connection, remainingData: remainingData, keepAlive: keepAlive, handled: handled) + } + return + } + + let queuedAt = DispatchTime.now().uptimeNanoseconds + renderQueue.async { [weak self] in + guard let self else { + connection.cancel() + return + } + // The wait is the queue telling us the lane is full. It shows up in + // the summary log as wait(max); a long one during a pinch means the + // gesture is generating tiles faster than the lane drains them, + // which is the intended trade — the UI stays smooth and the tiles + // arrive a beat later. + self.renderGate.wait() + let waitNanos = DispatchTime.now().uptimeNanoseconds - queuedAt + self.statsLock.lock() + self.activeRenders += 1 + self.statsMaxActive = max(self.statsMaxActive, self.activeRenders) + self.statsLock.unlock() + + let renderStart = DispatchTime.now().uptimeNanoseconds + let gone = { @Sendable in + switch connection.state { + case .cancelled, .failed: return true + default: return false + } + } + let outcome = self.resolveTile(path: path, sourceZoomOffset: sourceZoomOffset, isCancelled: gone) + let renderNanos = DispatchTime.now().uptimeNanoseconds - renderStart + self.renderGate.signal() + self.statsLock.lock() + self.activeRenders -= 1 + self.statsLock.unlock() + + self.record(outcome: outcome, path: path, waitNanos: waitNanos, renderNanos: renderNanos) + self.respond( + outcome: outcome, + connection: connection, + remainingData: remainingData, + keepAlive: keepAlive, + handled: handled + ) + } + } + + /// `/` under the files prefix. + private func serveFile( + routePath: String, + connection: NWConnection, + remainingData: Data, + keepAlive: Bool, + handled: Int + ) { + let routeId = String(routePath.prefix { $0 != "/" }) + let encoded = routePath.count > routeId.count ? String(routePath.dropFirst(routeId.count + 1)) : "" + let relative = encoded.removingPercentEncoding ?? "" + providersLock.lock() + let route = fileRoutes[routeId] + providersLock.unlock() + // No escaping the directory, whatever the request says. + let safe = !relative.isEmpty && !relative.split(separator: "/", omittingEmptySubsequences: false) + .contains { $0 == ".." || $0.isEmpty } + let finish: () -> Void = { [weak self, weak connection] in + guard let self, let connection else { return } + self.finishRequest(connection: connection, remainingData: remainingData, keepAlive: keepAlive, handled: handled) + } + if let route, safe, let data = try? Data(contentsOf: route.directory.appendingPathComponent(relative)) { + sendResponse( + connection: connection, + status: "200 OK", + contentType: Self.contentType(for: relative), + body: data, + keepAlive: keepAlive, + extraHeaders: ["Cache-Control": Self.longCacheControl], + completion: finish + ) + return + } + guard let route, safe, let fallback = route.fallback else { + sendResponse( + connection: connection, + status: "404 Not Found", + contentType: "text/plain", + body: Data("Not found".utf8), + keepAlive: keepAlive, + extraHeaders: ["Cache-Control": Self.noStoreCacheControl], + completion: finish + ) + return + } + renderQueue.async { [weak self] in + guard let self else { + connection.cancel() + return + } + let fetched = fallback(relative) + self.sendResponse( + connection: connection, + status: fetched == nil ? "404 Not Found" : "200 OK", + contentType: fetched == nil ? "text/plain" : Self.contentType(for: relative), + body: fetched ?? Data("Not found".utf8), + keepAlive: keepAlive, + extraHeaders: ["Cache-Control": Self.noStoreCacheControl], + completion: finish + ) + } + } + + private static func contentType(for path: String) -> String { + switch (path as NSString).pathExtension.lowercased() { + case "mvt": return "application/vnd.mapbox-vector-tile" + case "pbf": return "application/x-protobuf" + case "json": return "application/json" + case "png": return "image/png" + default: return "application/octet-stream" + } + } + + /// Feeds the counters and emits the once-a-second summary plus anomalies. + private func record(outcome: TileOutcome, path: String, waitNanos: UInt64, renderNanos: UInt64) { + MCLog.tileDetail(String( + format: "tile %@ wait=%.0fms render=%.0fms", + path, Double(waitNanos) / 1e6, Double(renderNanos) / 1e6 + )) + switch outcome { + case .notFound: + MCLog.tileServer("LocalTileServer: 404 \(path)") + case .failed: + // 503 -- the map will retry. If these repeat for the same path, + // the provider is failing deterministically; the path says which. + MCLog.tileServer("LocalTileServer: 503 \(path)") + case .abandoned: + MCLog.tileServer("LocalTileServer: abandoned \(path) after \(renderNanos / 1_000_000)ms") + case .empty, .tile: + break + } + + statsLock.lock() + statsRendered += 1 + statsRenderNanos += renderNanos + statsMaxWaitNanos = max(statsMaxWaitNanos, waitNanos) + if case .notFound = outcome { statsNotFound += 1 } + if case .failed = outcome { statsFailed += 1 } + if case .abandoned = outcome { statsAbandoned += 1 } + let now = DispatchTime.now().uptimeNanoseconds + let windowNanos = now - statsWindowStart + var line: String? + if windowNanos >= 1_000_000_000 { + line = String( + format: "LocalTileServer: %d tiles in %.1fs render=%.0fms wait(max)=%.0fms " + + "parallel(max)=%d/%d notFound=%d failed=%d abandoned=%d", + statsRendered, Double(windowNanos) / 1e9, + Double(statsRenderNanos) / 1e6, Double(statsMaxWaitNanos) / 1e6, + statsMaxActive, renderWidth, statsNotFound, statsFailed, statsAbandoned + ) + statsWindowStart = now + statsRendered = 0 + statsRenderNanos = 0 + statsMaxWaitNanos = 0 + statsMaxActive = 0 + statsNotFound = 0 + statsFailed = 0 + statsAbandoned = 0 + } + statsLock.unlock() + if let line { MCLog.tileServer(line) } + } + + private func respond( + outcome: TileOutcome, + connection: NWConnection, + remainingData: Data, + keepAlive: Bool, + handled: Int + ) { + if case .abandoned = outcome { + // Nobody is left to read an answer, and the only answer that would + // fit is "not found", which is a lie the map would believe. End the + // connection instead. + connection.cancel() + return + } + if case .failed = outcome { + sendResponse( + connection: connection, + status: "503 Service Unavailable", + contentType: "text/plain", + body: Data("Tile render failed".utf8), + keepAlive: keepAlive, + extraHeaders: ["Cache-Control": "no-store", "Retry-After": "1"] + ) { [weak self, weak connection] in + guard let self, let connection else { return } + self.finishRequest(connection: connection, remainingData: remainingData, keepAlive: keepAlive, handled: handled) + } + return + } + let tileResponse: TileResponse? = { + switch outcome { + case .tile(let response): + return response + case .empty(let pixelSize, let cacheControl): + // A transparent picture is what "nothing here" looks like. If + // the encoder cannot produce one the answer falls through to + // 404 below, which is the old behaviour and still wrong — but + // it only happens when PNG encoding itself is broken. + guard let png = TransparentTile.png(size: pixelSize) else { return nil } + return TileResponse(body: png, cacheControl: cacheControl) + default: + return nil + } + }() if let tileResponse { sendResponse( connection: connection, @@ -331,43 +730,64 @@ public final class LocalTileServer { } } - private func resolveTile(path: String) -> TileResponse? { + /// What a request resolved to. `abandoned` is deliberately not `notFound`: + /// the difference is what the map does next. + private enum TileOutcome { + case tile(TileResponse) + /// The provider had nothing to draw here. Answered with a transparent + /// tile: an empty spot is a real answer, and a cacheable one — the URL + /// carries the data version, so it is re-asked when the data changes. + /// Saying 404 or 503 instead is what leaves a hole in the map. + case empty(pixelSize: Int, cacheControl: String) + /// The path names nothing: bad route, bad coordinates. Answered 404, + /// and the map is right to stop asking. + case notFound + /// The provider could not draw it right now. Answered 503, which every + /// map SDK treats as retryable. A 404 here would be believed forever: + /// GMS caches "no tile" per coordinate, so one transient failure + /// becomes a permanent hole with its neighbours' icons cut at the edge. + case failed + /// The client gave up before the tile was drawn. + case abandoned + } + + private static func sourceZoomOffset(in url: String) -> Int { + let value = URLComponents(string: url)?.queryItems? + .first(where: { $0.name == "sourceZoomOffset" })?.value + return max(0, min(8, Int(value ?? "0") ?? 0)) + } + + private func resolveTile( + path: String, + sourceZoomOffset: Int = 0, + isCancelled: @escaping @Sendable () -> Bool + ) -> TileOutcome { let trimmed = path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - guard !trimmed.isEmpty else { return nil } + guard !trimmed.isEmpty else { return .notFound } let segments = trimmed.split(separator: "/").map(String.init) guard segments.count >= 6, segments[0] == "tiles" else { - return nil + return .notFound } let routeId = segments[1] - guard Int(segments[2]) != nil else { - return nil + guard let tileSize = Int(segments[2]) else { + return .notFound } let hasCacheKey = segments.count >= 7 let zIndex = hasCacheKey ? 4 : 3 let xIndex = hasCacheKey ? 5 : 4 let yIndex = hasCacheKey ? 6 : 5 guard let z = Int(segments[zIndex]), let x = Int(segments[xIndex]) else { - return nil + return .notFound } guard let coordinate = parseTileCoordinate(segments[yIndex]) else { - return nil + return .notFound } guard let provider = getProvider(routeId: routeId) else { - return nil - } - - MCLog.marker( - "LocalTileServer.resolveTile routeId=\(routeId) z=\(z) x=\(x) " + - "y=\(coordinate.y) ratio=\(coordinate.pixelRatio)x" - ) - guard let bytes = provider.renderTile( - request: TileRequest(x: x, y: coordinate.y, z: z, pixelRatio: coordinate.pixelRatio) - ) else { - return nil + return .notFound } let noStore: Bool = { @@ -376,7 +796,28 @@ public final class LocalTileServer { return forceNoStoreCache }() let cacheControl = noStore ? Self.noStoreCacheControl : Self.longCacheControl - return TileResponse(body: bytes, cacheControl: cacheControl) + + let bytes: Data? + do { + bytes = try provider.renderTile( + request: TileRequest(x: x, y: coordinate.y, z: z, pixelRatio: coordinate.pixelRatio, sourceZoomOffset: sourceZoomOffset), + isCancelled: isCancelled + ) + } catch { + MCLog.tileServer("LocalTileServer: render threw for \(trimmed): \(error)") + return isCancelled() ? .abandoned : .failed + } + guard let bytes else { + // Nothing to draw. Answered with pixels, not with "no tile" — + // unless nobody is waiting any more, in which case the only thing + // worth doing is not answering. + return isCancelled() ? .abandoned : .empty( + pixelSize: tileSize * coordinate.pixelRatio, + cacheControl: cacheControl + ) + } + guard !isCancelled() else { return .abandoned } + return .tile(TileResponse(body: bytes, cacheControl: cacheControl)) } private func sendResponse( @@ -422,6 +863,19 @@ public final class LocalTileServer { let cacheControl: String } + private struct Document { + let contentType: String + let body: Data + } + + private struct FileRoute { + let directory: URL + let fallback: (@Sendable (String) -> Data?)? + } + + private static let documentsPrefix = "docs" + private static let filesPrefix = "files" + private static let maxKeepAliveRequests = 10 private static let maxConcurrentConnections = 128 private static let maxRequestHeadBytes = 16 * 1024 diff --git a/Sources/MapConductorCore/MapViewBase.swift b/Sources/MapConductorCore/MapViewBase.swift index ac64f90..6482fab 100644 --- a/Sources/MapConductorCore/MapViewBase.swift +++ b/Sources/MapConductorCore/MapViewBase.swift @@ -120,6 +120,12 @@ open class MapViewCoordinatorBase: NSObject { super.init() } + /// What the map offers the ``MapViewStyle`` installed on it. + /// + /// Every coordinator that derives from this gets one; a provider whose + /// coordinator does not (ArcGIS, the template) holds its own. + public private(set) lazy var styleHost = MapViewStyleHost(serviceRegistry: state.serviceRegistry) + /// スクリーン空間の機能(InfoBubble・マーカーアニメーション)の入口に渡す関門。 /// /// 同期投影を持たないプロバイダで「黙って無反応」にならないようにする。 diff --git a/Sources/MapConductorCore/TileProvider.swift b/Sources/MapConductorCore/TileProvider.swift index 7f67a38..1bfae11 100644 --- a/Sources/MapConductorCore/TileProvider.swift +++ b/Sources/MapConductorCore/TileProvider.swift @@ -1,5 +1,45 @@ import Foundation public protocol TileProvider: AnyObject { + /** + Draws a tile, or returns nil when there is nothing to draw there. + + Nil is **"this spot is empty"**, and the server answers it with a + transparent tile — not a 404 and not a 503. A marker tile is empty only + *now*: data is still loading, a layer is mid-switch. Answering that with + "no tile" makes a map SDK remember a hole it never asks about again, and + the neighbouring tiles' overhang is cut off at its edge. + + A render that cannot be completed right now should **throw** instead; the + server turns that into a 503 the map knows to retry. + */ func renderTile(request: TileRequest) -> Data? + + /** + Draws a tile, giving up if the map stops waiting for it. + + - Parameter isCancelled: asked repeatedly while the tile is drawn. A map + that has panned away is not owed this tile, and drawing it anyway is not + free: renderers share a fixed number of slots, so a doomed tile is drawn + *instead of* one still on screen. + + Returning nil because the tile was abandoned is **not** the same as having + no tile. The server must not answer a cancelled request at all: a map that + believes a tile is missing stops asking for it and leaves whatever older + zoom it still has on screen — which is what a patchwork of two zoom levels + in one view looks like. + + Throwing means "not right now". A provider that never fails can stay + non-throwing: Swift lets a non-throwing method satisfy a throwing + requirement, so nothing already written needs to change. + */ + func renderTile(request: TileRequest, isCancelled: () -> Bool) throws -> Data? +} + +public extension TileProvider { + /// Providers that draw fast enough not to care can ignore cancellation; + /// this is what keeps them source-compatible. + func renderTile(request: TileRequest, isCancelled: () -> Bool) throws -> Data? { + renderTile(request: request) + } } diff --git a/Sources/MapConductorCore/TileRequest.swift b/Sources/MapConductorCore/TileRequest.swift index ef6992b..4e7c733 100644 --- a/Sources/MapConductorCore/TileRequest.swift +++ b/Sources/MapConductorCore/TileRequest.swift @@ -3,11 +3,14 @@ public struct TileRequest: Hashable { public let y: Int public let z: Int public let pixelRatio: Int + /// Render a coarser vector level within the same target grid. + public let sourceZoomOffset: Int - public init(x: Int, y: Int, z: Int, pixelRatio: Int = 1) { + public init(x: Int, y: Int, z: Int, pixelRatio: Int = 1, sourceZoomOffset: Int = 0) { self.x = x self.y = y self.z = z self.pixelRatio = pixelRatio + self.sourceZoomOffset = max(0, min(8, sourceZoomOffset)) } } diff --git a/Sources/MapConductorCore/TransparentTile.swift b/Sources/MapConductorCore/TransparentTile.swift new file mode 100644 index 0000000..4a23241 --- /dev/null +++ b/Sources/MapConductorCore/TransparentTile.swift @@ -0,0 +1,42 @@ +import Foundation + +/// 透明なタイル 1 枚。辺の長さごとに 1 度だけ作って使い回す。 +/// +/// 「ここには何も無い」は本物の答えで、透明な絵で返すのが正しい。`nil` を +/// 「タイルが無い」として 404 や 503 にすると、地図 SDK はそれを覚える/ +/// 引き直し続ける。前者は永久の穴になり、隣のタイルのはみ出し分だけが残るので +/// 穴の縁でアイコンが半分に切れて見える。 +/// +/// 置き場所が `LocalTileServer` の隣なのは、**空を絵にするのはサーバの仕事**だから。 +/// 以前は `MarkerTileRenderer` が自分で透明タイルを返していて、同じことをする +/// geojson / kml / groundimage / vectortile は nil のまま 503 になっていた。 +/// android-sdk-core の `TransparentTilePng` と対になっている。 +/// A transparent PNG per pixel size, encoded once and kept. +/// +/// What "nothing here" looks like to a map: the tile server answers an empty +/// spot with it, and a backend that asks for tiles nobody will see (ArcGIS's +/// 3D view loads every ancestor of the level on screen) is handed one instead +/// of a render. +public enum TransparentTile { + private static let lock = NSLock() + nonisolated(unsafe) private static var cache: [Int: Data] = [:] + + /// 一辺 `size` ピクセルの透明 PNG。作れなければ nil。 + public static func png(size: Int) -> Data? { + let clamped = min(max(size, 1), maxSize) + lock.lock() + defer { lock.unlock() } + if let cached = cache[clamped] { return cached } + var pixels = [UInt8](repeating: 0, count: clamped * clamped * 4) + let png = pixels.withUnsafeMutableBytes { raw -> Data? in + guard let base = raw.baseAddress else { return nil } + return TilePngEncoder.encode(rgba: base, width: clamped, height: clamped, premultiplied: true) + } + if let png { cache[clamped] = png } + return png + } + + /// 4096px タイルまで。これを超える要求は壊れた URL なので、確保量が + /// 二乗で効くこの経路では受けない。 + private static let maxSize = 4096 +} diff --git a/Sources/MapConductorCore/circle/CircleState.swift b/Sources/MapConductorCore/circle/CircleState.swift index 85116a1..960f958 100644 --- a/Sources/MapConductorCore/circle/CircleState.swift +++ b/Sources/MapConductorCore/circle/CircleState.swift @@ -31,16 +31,20 @@ public typealias OnCircleEventHandler = (CircleEvent) -> Void public final class CircleState: ObservableObject, Identifiable, Equatable, Hashable { public let id: String - @Published public var center: GeoPointProtocol - @Published public var radiusMeters: Double - @Published public var geodesic: Bool - @Published public var clickable: Bool - @Published public var strokeColor: UIColor - @Published public var strokeWidth: Double - @Published public var fillColor: UIColor - @Published public var extra: Any? - @Published public var zIndex: Int? - @Published public var onClick: OnCircleEventHandler? + /// Writes to the fields below are announced here rather than discovered by + /// subscribing to each state. See ``StateMutationSignal``. + public let mutations = StateMutationSignal() + + @Published public var center: GeoPointProtocol { didSet { mutations.notifyMutated() } } + @Published public var radiusMeters: Double { didSet { mutations.notifyMutated() } } + @Published public var geodesic: Bool { didSet { mutations.notifyMutated() } } + @Published public var clickable: Bool { didSet { mutations.notifyMutated() } } + @Published public var strokeColor: UIColor { didSet { mutations.notifyMutated() } } + @Published public var strokeWidth: Double { didSet { mutations.notifyMutated() } } + @Published public var fillColor: UIColor { didSet { mutations.notifyMutated() } } + @Published public var extra: Any? { didSet { mutations.notifyMutated() } } + @Published public var zIndex: Int? { didSet { mutations.notifyMutated() } } + @Published public var onClick: OnCircleEventHandler? { didSet { mutations.notifyMutated() } } public init( center: GeoPointProtocol, diff --git a/Sources/MapConductorCore/debug/MCLog.swift b/Sources/MapConductorCore/debug/MCLog.swift index 8ab5427..4087920 100644 --- a/Sources/MapConductorCore/debug/MCLog.swift +++ b/Sources/MapConductorCore/debug/MCLog.swift @@ -18,9 +18,10 @@ public enum MCLog { private static let markerLogger = Logger(subsystem: subsystem, category: "Marker") private static let mapLogger = Logger(subsystem: subsystem, category: "MapView") + private static let tileLogger = Logger(subsystem: subsystem, category: "TileServer") public static func marker(_ message: String) { - // guard isEnabled else { return } + guard isEnabled else { return } markerLogger.debug("\(message, privacy: .public)") } @@ -28,5 +29,41 @@ public enum MCLog { guard isEnabled else { return } mapLogger.debug("\(message, privacy: .public)") } + + /// タイルサーバの節目 -- 1 秒ごとの集計、404、abandoned、詰まり。 + /// + /// Debug ビルドでは常に出す。「一部のタイルが出ない」「ピンチが重い」は + /// どちらも**あとから**報告される症状で、そのときにはもう再現の環境変数を + /// 立て直せない。行数は集計と異常だけなので秒に数行、絞る理由がない。 + /// 1 リクエスト 1 行の詳細のほうは `isEnabled` で絞る(`tileDetail`)。 + public static func tileServer(_ message: String) { + #if DEBUG + tileLogger.info("\(message, privacy: .public)") + #else + guard isEnabled else { return } + tileLogger.debug("\(message, privacy: .public)") + #endif + } + + /// タイル 1 リクエスト 1 行の詳細。MAPCONDUCTOR_DEBUG_LOG=1 のときだけ。 + public static func tileDetail(_ message: String) { + guard isEnabled else { return } + tileLogger.debug("\(message, privacy: .public)") + } + + private static let probeLogger = Logger(subsystem: subsystem, category: "Probe") + + /// メインスレッドを塞いだ区間の報告。呼び出し側が閾値を超えたときだけ呼ぶ。 + /// + /// 「ピンチが重い」の類いは、重かった**その場**のログしか証拠にならない。 + /// Debug ビルドでは常時出す。閾値付きなので、健康なら 1 行も出ない。 + public static func probe(_ message: String) { + #if DEBUG + probeLogger.info("\(message, privacy: .public)") + #else + guard isEnabled else { return } + probeLogger.debug("\(message, privacy: .public)") + #endif + } } diff --git a/Sources/MapConductorCore/geocell/HexCellRegistry.swift b/Sources/MapConductorCore/geocell/HexCellRegistry.swift index 15bcc0e..bd67d63 100644 --- a/Sources/MapConductorCore/geocell/HexCellRegistry.swift +++ b/Sources/MapConductorCore/geocell/HexCellRegistry.swift @@ -140,6 +140,14 @@ public final class HexCellRegistry { return kdTree?.withinRadiusWithDistance(query: geocell.projection.project(point), radius: radius) ?? [] } + /// Cells within `radius`, unordered — see `KDTree.withinRadius`. + public func findWithinRadius(point: GeoPointProtocol, radius: Double) -> [HexCell] { + lock.lock() + defer { lock.unlock() } + rebuildIfNeeded() + return kdTree?.withinRadius(query: geocell.projection.project(point), radius: radius) ?? [] + } + public func all() -> [HexCell] { lock.lock() defer { lock.unlock() } diff --git a/Sources/MapConductorCore/geocell/KDTree.swift b/Sources/MapConductorCore/geocell/KDTree.swift index cf0f733..00f221a 100644 --- a/Sources/MapConductorCore/geocell/KDTree.swift +++ b/Sources/MapConductorCore/geocell/KDTree.swift @@ -143,6 +143,47 @@ public final class KDTree: Sendable { return results } + /// Cells within `radius`, unordered. + /// + /// The sorted, distance-carrying variant exists for callers that want the + /// nearest first. Bounds queries do not: they use every cell and ignore the + /// distances, and sorting them is the dominant cost of the search. A cell + /// carries a String, so sorting 20k of them moves 20k refcounted structs + /// through ~286k comparisons — 42 ms of a 64 ms query on an iPad Pro. + public func withinRadius(query: CGPoint, radius: Double) -> [HexCell] { + precondition(radius >= 0, "Radius must be non-negative") + guard let root else { return [] } + + var results: [HexCell] = [] + withinRadiusUnordered(node: root, query: query, radiusSq: radius * radius, results: &results) + return results + } + + private func withinRadiusUnordered( + node: Node, + query: CGPoint, + radiusSq: Double, + results: inout [HexCell] + ) { + if squaredDistance(query, node.cell.centerXY) <= radiusSq { + results.append(node.cell) + } + + let axis = node.axis + let queryVal = axis == 0 ? query.x : query.y + let nodeVal = axis == 0 ? node.cell.centerXY.x : node.cell.centerXY.y + let nearChild = queryVal < nodeVal ? node.left : node.right + let farChild = queryVal < nodeVal ? node.right : node.left + + if let nearChild { + withinRadiusUnordered(node: nearChild, query: query, radiusSq: radiusSq, results: &results) + } + let axisDistance = queryVal - nodeVal + if axisDistance * axisDistance <= radiusSq, let farChild { + withinRadiusUnordered(node: farChild, query: query, radiusSq: radiusSq, results: &results) + } + } + private func withinRadius(node: Node, query: CGPoint, radiusSq: Double, results: inout [HexCellWithDistance]) { let distSq = squaredDistance(query, node.cell.centerXY) if distSq <= radiusSq { diff --git a/Sources/MapConductorCore/groundimage/GroundImageState.swift b/Sources/MapConductorCore/groundimage/GroundImageState.swift index 886faf3..c985a82 100644 --- a/Sources/MapConductorCore/groundimage/GroundImageState.swift +++ b/Sources/MapConductorCore/groundimage/GroundImageState.swift @@ -27,12 +27,16 @@ public typealias OnGroundImageEventHandler = (GroundImageEvent) -> Void public final class GroundImageState: ObservableObject, Identifiable, Equatable, Hashable { public let id: String - @Published public var bounds: GeoRectBounds - @Published public var image: UIImage - @Published public var opacity: Double - @Published public var tileSize: Int - @Published public var extra: Any? - @Published public var onClick: OnGroundImageEventHandler? + /// Writes to the fields below are announced here rather than discovered by + /// subscribing to each state. See ``StateMutationSignal``. + public let mutations = StateMutationSignal() + + @Published public var bounds: GeoRectBounds { didSet { mutations.notifyMutated() } } + @Published public var image: UIImage { didSet { mutations.notifyMutated() } } + @Published public var opacity: Double { didSet { mutations.notifyMutated() } } + @Published public var tileSize: Int { didSet { mutations.notifyMutated() } } + @Published public var extra: Any? { didSet { mutations.notifyMutated() } } + @Published public var onClick: OnGroundImageEventHandler? { didSet { mutations.notifyMutated() } } public init( bounds: GeoRectBounds, diff --git a/Sources/MapConductorCore/map/ApplyStyleMutations.swift b/Sources/MapConductorCore/map/ApplyStyleMutations.swift new file mode 100644 index 0000000..db469dc --- /dev/null +++ b/Sources/MapConductorCore/map/ApplyStyleMutations.swift @@ -0,0 +1,135 @@ +import Foundation + +/** + The seven things a renderer has to be able to do for a style to be adjusted + in place. + + A provider implements these against its own SDK and gets + ``applyStyleMutations(_:_:)`` for everything above them — what each mutation + means, which ones can be skipped, and keeping one bad property from costing + the rest. MapLibre and Mapbox name their style APIs differently, so the + adapter has to be per-provider; the decisions do not. + + Values arrive as JSON text, which is what crossed the FFI boundary: the + provider turns it into whatever its SDK wants — an `NSExpression` for + MapLibre here, a `Value` for Mapbox. + + android-sdk-core carries the same protocol, and js-sdk-core does the same + work structurally — there, MapLibre GL JS, Mapbox GL JS and the MapTiler SDK + happen to share their method names, so one applier serves all three. + */ +public protocol StyleMutationTarget { + /** + Whether the renderer has a style up to be changed. + + **False is not failure, and must not be reported as one.** A renderer + with no style loaded yet cannot be asked about layers, so without this + every mutation came back as one the map "cannot" make — which is how an + app got told "N adjustments were not applied" about a map that was + merely still loading, and, under ``VectorStyleMutationSupport``'s + reloading policy, had the adjusted document handed to it for no reason. + Nothing is lost by waiting: the store puts the whole set back when the + style loads. + + Implement it. The default is for a target that is always ready; a + provider whose style arrives asynchronously — which is both of them — + answers whether it has one. + */ + var isReady: Bool { get } + + /** + False for a layer the loaded style does not have. + + Asked only when ``isReady``, so it is about the style's contents and + never about whether there is a style: answering false for "not loaded + yet" is what ``isReady`` exists to stop. + + Only the four property changes ask. ``addLayer(_:_:_:)`` and + ``removeLayer(_:)`` guard themselves, because "is it already there" is a + question only the renderer can answer about a layer it owns. + */ + func hasLayer(_ layerId: String) -> Bool + + func setPaint(_ layerId: String, _ key: String, _ valueJson: String) throws + + func setLayout(_ layerId: String, _ key: String, _ valueJson: String) throws + + /// `"null"` removes the filter. + func setFilter(_ layerId: String, _ filterJson: String) throws + + func setZoomRange(_ layerId: String, _ minZoom: Double, _ maxZoom: Double) throws + + /** + Adds a layer, or does nothing if the renderer already has one with this + id — which is the usual case, because a style reload puts the whole set + back. + */ + func addLayer(_ layerId: String, _ layerJson: String, _ beforeId: String?) throws + + /// A layer the renderer does not have is not an error. + func removeLayer(_ layerId: String) throws +} + +extension StyleMutationTarget { + /// For a target with nothing to wait for. Every real provider overrides it. + public var isReady: Bool { true } +} + +/** + Applies style mutations to a live renderer through `target`. + + The body of a provider's ``VectorStyleMutationSupport``: a provider wires + this into a ``StyleMutationStore`` and registers that, and every provider + then behaves identically by construction. + + - Returns: the mutations it could not apply. A layer the loaded style does + not have, or a property the renderer rejected: the caller is told rather + than left to assume the map looks the way the rules asked. Nothing is + thrown — one bad property must not cost the other three hundred. Empty + while the renderer has no style up, because then there is nothing it + could not do — only something it has not done yet. + */ +public func applyStyleMutations( + _ target: StyleMutationTarget, + _ mutations: [StyleMutation] +) -> [StyleMutation] { + // Not ready is not a refusal. See ``StyleMutationTarget/isReady``. + guard target.isReady else { return [] } + return mutations.filter { mutation in + do { + return try !applyOne(target, mutation) + } catch { + return true + } + } +} + +private func applyOne(_ target: StyleMutationTarget, _ mutation: StyleMutation) throws -> Bool { + switch mutation { + case let .setPaint(layerId, key, value, _): + guard target.hasLayer(layerId) else { return false } + try target.setPaint(layerId, key, value) + case let .setLayout(layerId, key, value, _): + guard target.hasLayer(layerId) else { return false } + try target.setLayout(layerId, key, value) + case let .setFilter(layerId, value, _): + guard target.hasLayer(layerId) else { return false } + try target.setFilter(layerId, value) + case let .setZoomRange(layerId, minZoom, maxZoom, _, _): + guard target.hasLayer(layerId) else { return false } + // The spec's own defaults, for a layer that named neither. Every + // renderer takes both ends at once, so a mutation that changes one + // has to restate the other. + try target.setZoomRange(layerId, minZoom ?? 0, maxZoom ?? 24) + case let .addLayer(layerId, layer, beforeId): + try target.addLayer(layerId, layer, beforeId) + case let .removeLayer(layerId): + try target.removeLayer(layerId) + } + return true +} + +/// For a message naming what would not apply. +public func describeStyleMutations(_ mutations: [StyleMutation]) -> String { + mutations.map(\.target).joined(separator: ", ") +} diff --git a/Sources/MapConductorCore/map/BlankMapStyle.swift b/Sources/MapConductorCore/map/BlankMapStyle.swift new file mode 100644 index 0000000..20ef04c --- /dev/null +++ b/Sources/MapConductorCore/map/BlankMapStyle.swift @@ -0,0 +1,33 @@ +import Foundation + +/// A MapLibre-style map with nothing in it but a background. +/// +/// What a provider shows for "no basemap" when it is built on MapLibre or takes +/// a MapLibre style URL (Mapbox, MapTiler, TomTom). A map that draws its whole +/// viewport itself -- vector tiles rendered on the device, say -- fetches and +/// paints the basemap under it for nobody; this is the style that fetches +/// nothing. android-sdk-core and js-sdk-core carry the same style. +public enum BlankMapStyle { + /// The one colour the style has. + public static let backgroundColor = "#f2efe9" + + public static let json = """ + {"version":8,"name":"blank","sources":{},"layers":[{"id":"background","type":"background","paint":{"background-color":"#f2efe9"}}]} + """ + + /// The style as a file, for SDKs that only take a URL. + /// + /// Written once into Caches; the vendor SDKs read `file:` URLs but none + /// of them read `data:` ones, and a Swift package cannot rely on a bundled + /// resource reaching every distribution (CocoaPods source pods, prebuilt + /// frameworks). + public static let fileURL: URL = { + let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + ?? FileManager.default.temporaryDirectory + let url = dir.appendingPathComponent("mapconductor-blank-style.json") + if (try? Data(contentsOf: url)) != json.data(using: .utf8) { + try? json.data(using: .utf8)?.write(to: url, options: .atomic) + } + return url + }() +} diff --git a/Sources/MapConductorCore/map/MapServiceRegistry.swift b/Sources/MapConductorCore/map/MapServiceRegistry.swift index 371e6cc..c6a5f43 100644 --- a/Sources/MapConductorCore/map/MapServiceRegistry.swift +++ b/Sources/MapConductorCore/map/MapServiceRegistry.swift @@ -246,6 +246,11 @@ public extension MutableMapServiceRegistry { func removeProviderRegistrations() { remove(MarkerRenderingSupportKey.self) remove(OverlayControllerRegistryKey.self) + // ArcGIS の 3D と 2D は同じ state を共有できる(サンプルがそうしている)。 + // 3D だけが「タイルは 256pt」を宣言するので、外し忘れると 2D に切り替えた + // あとも 256 が残り、2D は同じ画面を 4 倍の枚数で覆うことになる。 + remove(RasterTilePreferenceKey.self) + remove(VectorStyleSupportKey.self) } } diff --git a/Sources/MapConductorCore/map/MapViewStyle.swift b/Sources/MapConductorCore/map/MapViewStyle.swift new file mode 100644 index 0000000..59dafdb --- /dev/null +++ b/Sources/MapConductorCore/map/MapViewStyle.swift @@ -0,0 +1,155 @@ +import Foundation + +/** + How a map looks, as a property of the map rather than a layer inside it. + + A vector style *is* the basemap, so passing one to + `XxxMapView(state:style:)` says what it is rather than mounting something + over it. What happens underneath depends on the backend and the app does not + have to know: MapLibre, Mapbox and MapTiler are told the style and draw it + themselves; Google Maps, MapKit, HERE, ArcGIS and the rest are handed raster + tiles rendered from the same style on the device. Either way the same + adjustments produce the same map. + + A protocol rather than a type, for two reasons. Core must not depend on the + style compiler or the rasteriser -- both are optional modules, and one of + them is a megabyte of native code -- so what core holds is the seam and the + modules bring the implementation. And a provider's own styling, which is + nothing like a MapLibre style, can be added later as another conformance + without this changing. + + The concrete types live in the vector style module + (`MapConductorVectorStyle`); an app does not conform to this. + + android-sdk-core and js-sdk-core carry the same seam. + */ +public protocol MapViewStyle { + /** + What identifies this style's content. + + The map re-installs a style whose key changed and leaves alone one whose + key did not. Two values describing the same thing must have the same + key: a style built inline in a `body` is a new value on every + evaluation -- which is exactly what a colour picker produces -- and + keying off identity would reinstall the whole style on every unrelated + state change. + */ + var key: String { get } + + /** + Puts the style on the map. Called once the map has a style loaded, and + again if it loads another one. + + - Returns: the ticket that takes it off again. Disposing must leave the + map as it was found: a style that cannot be fully undone has no + business being installed, because the app will switch away from it. + */ + func install(host: MapStyleHost) -> MapStyleInstallation +} + +/** + A ticket to undo, and a chance to take a change without tearing down. + + Returned by anything that sets something up on a map. A value holding + closures rather than a protocol: everything that hands one out would + otherwise need a type whose only job is to remember how to undo one thing. + */ +public struct MapStyleInstallation { + private let onDispose: () -> Void + private let onUpdate: (MapViewStyle) -> Bool + + /** + - Parameter update: takes `next` in place, when it is near enough to + what is installed, and answers whether it did. + + Without this the whole point is lost. A style whose key changed is + installed again, and installing a style means handing the map a + document -- which is the reload this design exists to avoid. Moving a + colour slider changes the adjustments and nothing else, so the + installed style answers true, applies the difference, and the map + never reloads anything. False is always correct, only slower: the + caller then disposes and installs from scratch. + */ + public init( + dispose: @escaping () -> Void, + update: @escaping (MapViewStyle) -> Bool = { _ in false } + ) { + onDispose = dispose + onUpdate = update + } + + public func dispose() { + onDispose() + } + + public func update(_ next: MapViewStyle) -> Bool { + onUpdate(next) + } + + /// For a style that set nothing up. + public static let none = MapStyleInstallation(dispose: {}) +} + +/** + What a map offers to the style installed on it. + + The provider implements this; a ``MapViewStyle`` uses it and never touches a + provider type. That is what keeps one style implementation working on every + backend. + */ +public protocol MapStyleHost: AnyObject { + /** + Where to look for what this map can do: + ``VectorStyleSupportKey`` to hand over a style document, + ``VectorStyleMutationSupportKey`` to change a loaded one in place, + ``RasterTilePreferenceKey`` for the tile size it would rather have. + */ + var serviceRegistry: MapServiceRegistry { get } + + /// For serving a style document, or tiles rendered from one, to the map. + var tileServer: LocalTileServer { get } + + /** + The vector style the map is drawing now, when it is drawing one. + + Nil on every backend that has no such thing, which is most of them. It + is how a rule set that adjusts "whatever is showing" finds something to + adjust: the style has to be read before a rule can be matched against + its layers, and only the map knows where it came from. + */ + var vectorStyleUrl: String? { get } + + /** + Runs `block` whenever the map finishes loading a style, including the + one it has now if it already has one. + + Everything a style installs on a live renderer is lost when that + renderer loads a document again, for any reason. This is how it gets put + back. + */ + func onStyleLoaded(_ block: @escaping () -> Void) -> MapStyleInstallation + + /** + Adds or updates a raster layer the app did not declare. + + The raster path needs somewhere to put its tiles, and that cannot be the + view's content: the style is a property of the view, so it is installed + from outside. Marker tiling already mounts its layer the same way. + */ + func upsertRaster(_ state: RasterLayerState) + + func removeRaster(id: String) + + /** + Says what the app should know: a rule that matched nothing, an + adjustment this backend cannot make, a style that could not be read. + + Routed to whatever the host gave the style, so a module can report + without knowing how the app logs. + */ + func report(_ diagnostics: [String]) +} + +public extension MapStyleHost { + var vectorStyleUrl: String? { nil } +} diff --git a/Sources/MapConductorCore/map/MapViewStyleHost.swift b/Sources/MapConductorCore/map/MapViewStyleHost.swift new file mode 100644 index 0000000..4ac3825 --- /dev/null +++ b/Sources/MapConductorCore/map/MapViewStyleHost.swift @@ -0,0 +1,312 @@ +import Foundation + +/** + A controller that can mount a raster layer the view's content did not + declare. + + ``RasterLayerController`` already does both of these; this is the name that + lets a caller reach them without knowing which provider's controller it has. + `compositionRasterLayers` replaces the whole set with what the content + declared, so anything mounted from outside would be swept away on the next + pass — `upsert` keeps its ids out of that sweep. + + Two callers need it: marker tiling, and a ``MapViewStyle`` on a backend that + cannot draw a vector style and has to be handed tiles rendered from one. + */ +public protocol RasterLayerMounting: AnyObject { + func upsert(state: RasterLayerState) async + func removeById(_ id: String) async +} + +/** + What a map offers the style installed on it, and the bookkeeping that keeps + it installed. + + One class for every provider. Everything it needs that differs per backend + is reached through the service registry, which every provider already fills + in — ``VectorStyleSupportKey`` for handing over a document, + ``VectorStyleMutationSupportKey`` for changing a loaded one, + ``OverlayControllerRegistryKey`` for mounting tiles. Adding a provider needs + nothing here. + + Most coordinators get one from ``MapViewCoordinatorBase``; a provider whose + coordinator is not one (ArcGIS, the template) holds its own. + + android-sdk's `MapViewStyleEffect` and js-sdk-react's `useMapViewStyle` do + the same work; the shape differs because each framework owns its lifecycle + differently. + */ +public final class MapViewStyleHost: MapStyleHost { + public let serviceRegistry: MapServiceRegistry + + private var installation: MapStyleInstallation? + private var installedKey: String? + private var diagnosticsHandler: (([String]) -> Void)? + + /// How many updates in a row the installed style has refused. See ``apply(_:onDiagnostics:)``. + private var refusedUpdates = 0 + private static let churnThreshold = 5 + + /// Sticky, because a provider can finish loading before anything has + /// asked to be told about it. + private var didLoadStyle = false + private var styleLoadedListeners: [UUID: () -> Void] = [:] + + public init(serviceRegistry: MapServiceRegistry) { + self.serviceRegistry = serviceRegistry + } + + deinit { + installation?.dispose() + } + + // MARK: - Keeping a style installed + + /** + Keeps `style` installed on this map. + + Called from the representable's `makeUIView` and `updateUIView`, which + is every evaluation of the app's `body` — so most calls do nothing. + Three things have to line up and none of them is the view update: + + - **A style whose key did not change must be left alone.** A style built + inline in a `body` is a new value on every evaluation — which is + exactly what a colour picker produces — so comparing values would + reinstall on every unrelated state change. + - **A changed style is usually an adjustment, not a new style.** + Disposing and installing again hands the map a document and reloads + it, which is the cost this whole design exists to avoid. What is + installed is offered the new one first and only replaced if it + refuses. + - **Nil means take it off.** An app that stops passing a style gets the + map it had before, not the one the style left behind. + */ + public func apply(_ style: MapViewStyle?, onDiagnostics: (([String]) -> Void)? = nil) { + diagnosticsHandler = onDiagnostics + guard let style else { + dispose() + return + } + guard let installed = installation else { + installation = style.install(host: self) + installedKey = style.key + return + } + // Nothing changed at all. The key covers the document *and* the + // rules, so an equal key means there is no work -- and doing it + // anyway is not merely wasteful: recompiling re-reports the + // diagnostics, an app that renders them sets state, that evaluates + // the body again, and the two spin against each other. + if installedKey == style.key { return } + + // The adjustments moved. Offer them to what is installed: taking + // them in place is what keeps a slider from reloading the map. + if installed.update(style) { + installedKey = style.key + refusedUpdates = 0 + return + } + + // Refused -- correct, only slower. Refusing every time means the + // style cannot recognise its own successor and the map is rebuilt + // on each change; the usual cause is a value the style compares by + // identity being rebuilt too, a rasteriser held in a `let` on a + // `View` rather than `@State`. + refusedUpdates += 1 + if refusedUpdates == Self.churnThreshold { + MCLog.map( + "style: the installed style has refused \(Self.churnThreshold) updates in a row, " + + "so the map is being torn down and rebuilt on every change. Hold the " + + "rasteriser (and anything else the style compares by identity) in @State " + + "rather than rebuilding it." + ) + } + installed.dispose() + installation = style.install(host: self) + installedKey = style.key + } + + /// Takes the style off. The representable calls this when the view goes. + public func dispose() { + installation?.dispose() + installation = nil + installedKey = nil + } + + /** + Called by the provider once the map has a style up **and** the overlays + are back on it. + + Not when the load starts, and not before the provider has put its own + layers back: a listener that patches a style mid-rebuild patches + something that is about to be replaced. + */ + public func notifyStyleLoaded() { + didLoadStyle = true + styleLoadedListeners.values.forEach { $0() } + } + + // MARK: - MapStyleHost + + public var tileServer: LocalTileServer { TileServerRegistry.get() } + + public var vectorStyleUrl: String? { + serviceRegistry.get(VectorStyleSupportKey.self)?.currentStyleUrl + } + + /** + Runs `block` every time this map finishes loading a style, and once now + if it already has one. + + Everything a style put on a live renderer — paint properties, a layer it + added — is gone when that renderer loads a document again, for any + reason: the app switching basemap, a design change, the provider + rebuilding its view. This is how it goes back on, and it is a registry + rather than one listener because both the mutation store and the style + installation need to hear it. + + A provider with no such thing (Google Maps, MapKit, HERE...) never calls + ``notifyStyleLoaded()``, and a style installed on it is one that was + rasterised into tiles — which survives a basemap change by itself. + */ + public func onStyleLoaded(_ block: @escaping () -> Void) -> MapStyleInstallation { + let token = UUID() + styleLoadedListeners[token] = block + if didLoadStyle { block() } + return MapStyleInstallation(dispose: { [weak self] in + self?.styleLoadedListeners.removeValue(forKey: token) + }) + } + + public func upsertRaster(_ state: RasterLayerState) { + let controllers = rasterMounts() + guard !controllers.isEmpty else { + noRasterMount(state.id) + return + } + Task { @MainActor in + for controller in controllers { await controller.upsert(state: state) } + } + } + + public func removeRaster(id: String) { + let controllers = rasterMounts() + guard !controllers.isEmpty else { return } + Task { @MainActor in + for controller in controllers { await controller.removeById(id) } + } + } + + public func report(_ diagnostics: [String]) { + guard !diagnostics.isEmpty else { return } + if let diagnosticsHandler { + diagnosticsHandler(diagnostics) + } else { + // Never dropped. A rule that matched nothing, or an adjustment + // this backend could not make, is the kind of thing that + // otherwise shows up as "the style nearly worked" with no way to + // find out why. + diagnostics.forEach { MCLog.map("style: \($0)") } + } + } + + /** + The raster controllers this map registered. + + Reached through the overlay registry rather than a per-provider + property, so adding a provider needs nothing here. + */ + /** + Says so when a provider has nowhere to mount one. + + Every provider registers its raster controller in the overlay registry + and so gets ``upsertRaster(_:)`` for free. One that does not would + otherwise show an empty map and say nothing, which is the failure this + whole design is built against. android-sdk reports the same thing from + `BaseMapViewController`. + */ + /** + Said out loud, not logged. + + With no raster controller there is nowhere to put the tiles, so the + style compiles, reports its rules, and draws nothing -- the map simply + keeps the backend's own basemap. Behind a log line that only appears + with `MAPCONDUCTOR_DEBUG_LOG=1`, that reads as "the style silently does + not work", which is exactly how it read for ArcGIS. + */ + private func noRasterMount(_ id: String) { + report([ + "raster layer \(id) was not mounted: this provider registered no raster controller " + + "in its overlay registry, so there is nowhere to put the tiles this style " + + "was rendered into" + ]) + } + + private func rasterMounts() -> [any RasterLayerMounting] { + guard let registry = serviceRegistry.get(OverlayControllerRegistryKey.self) else { return [] } + return registry.all().compactMap { $0 as? any RasterLayerMounting } + } +} + +public extension MapViewCoordinatorBase { + /// See ``MapViewStyleHost/apply(_:onDiagnostics:)``. + func applyStyle(_ style: MapViewStyle?, onDiagnostics: (([String]) -> Void)? = nil) { + styleHost.apply(style, onDiagnostics: onDiagnostics) + } + + /// See ``MapViewStyleHost/dispose()``. + func disposeStyle() { + styleHost.dispose() + } + + /// See ``MapViewStyleHost/notifyStyleLoaded()``. + func notifyStyleLoaded() { + styleHost.notifyStyleLoaded() + } +} + +#if canImport(UIKit) + import SwiftUI + import UIKit + + /** + Keeps a ``MapViewStyle`` applied from inside a pure-SwiftUI map. + + Most providers wrap a `UIView`, so their representable's `updateUIView` + is already the hook that runs on every evaluation of the app's `body`. + ArcGIS and the template draw with SwiftUI views instead and have no such + hook — and `body` itself is the wrong place, because reading a style + there and installing it would be a side effect in a view's description + of itself. + + So they mount this: an empty view whose only job is to have an + `updateUIView`. Zero-sized and hidden, it draws nothing and lays nothing + out. + */ + public struct MapViewStyleApplier: UIViewRepresentable { + private let host: MapViewStyleHost + private let style: MapViewStyle? + private let onDiagnostics: (([String]) -> Void)? + + public init( + host: MapViewStyleHost, + style: MapViewStyle?, + onDiagnostics: (([String]) -> Void)? = nil + ) { + self.host = host + self.style = style + self.onDiagnostics = onDiagnostics + } + + public func makeUIView(context _: Context) -> UIView { + let view = UIView(frame: .zero) + view.isHidden = true + view.isUserInteractionEnabled = false + return view + } + + public func updateUIView(_: UIView, context _: Context) { + host.apply(style, onDiagnostics: onDiagnostics) + } + } +#endif diff --git a/Sources/MapConductorCore/map/StyleMutation.swift b/Sources/MapConductorCore/map/StyleMutation.swift new file mode 100644 index 0000000..b0c1c02 --- /dev/null +++ b/Sources/MapConductorCore/map/StyleMutation.swift @@ -0,0 +1,222 @@ +import Foundation + +/** + One change to one layer of a vector style the map has already loaded. + + This is what lets a style be adjusted without being reloaded. Handing a map + a new style document is the obvious way to change how it looks, and on every + provider it is the expensive way: the renderer drops its tiles, re-reads the + document, re-tessellates, and the overlays the app added have to be put + back. A paint property set in place costs none of that. + + Mutations are produced by the vector style compiler from the same evaluation + that produces the adjusted document, so the two can never disagree about + what the style should look like. A provider that can take them declares + ``VectorStyleMutationSupport``. + + ## Why the values are JSON text + + A style property's value is anything the style spec allows: a string, a + number, or an expression tree. Each provider needs it in its own form -- + MapLibre here wants an `NSExpression`, MapLibre on Android a Gson tree, + MapLibre in a browser the plain parsed value -- so there is no shared typed + representation that saves anyone a conversion. Text is the one form all + three can start from, it is what crossed the FFI boundary anyway, and it is + `Sendable`, which `Any` is not. + + `"null"` means the property had no value, which is also how a renderer is + told to go back to the spec default. + + android-sdk-core and js-sdk-core carry the same type. + */ +public enum StyleMutation: Equatable, Sendable { + case setPaint(layerId: String, key: String, value: String, previous: String) + case setLayout(layerId: String, key: String, value: String, previous: String) + case setFilter(layerId: String, value: String, previous: String) + case setZoomRange( + layerId: String, + minZoom: Double?, + maxZoom: Double?, + previousMinZoom: Double?, + previousMaxZoom: Double? + ) + /** + Adds a layer the style did not have. + + Used for one thing: a background under a style that has none, so that + "everything else muted" looks the same whether the style is drawn by the + map or rasterised into tiles over another basemap. Without it the raster + path leaves the gaps transparent and the two disagree. + */ + case addLayer(layerId: String, layer: String, beforeId: String?) + case removeLayer(layerId: String) + + /// The layer this acts on. Every mutation names one. + public var layerId: String { + switch self { + case let .setPaint(layerId, _, _, _), + let .setLayout(layerId, _, _, _), + let .setFilter(layerId, _, _), + let .setZoomRange(layerId, _, _, _, _), + let .removeLayer(layerId), + let .addLayer(layerId, _, _): + return layerId + } + } + + /** + What this mutation occupies, for working out which of two sets of + mutations replace each other. Two mutations with the same target are the + same change with different values. + */ + public var target: String { + switch self { + case let .setPaint(layerId, key, _, _): return "paint|\(layerId)|\(key)" + case let .setLayout(layerId, key, _, _): return "layout|\(layerId)|\(key)" + case let .setFilter(layerId, _, _): return "filter|\(layerId)" + case let .setZoomRange(layerId, _, _, _, _): return "zoom|\(layerId)" + case let .addLayer(layerId, _, _): return "add|\(layerId)" + case let .removeLayer(layerId): return "add|\(layerId)" + } + } + + /** + What this mutation changes, named the way the style spec names it. + + For telling an app *what* a renderer would not do. The layer is the + wrong thing to name on its own: when a backend has no property for a + spec key, the same key fails on every layer that uses it, so a list of + layers is hundreds of names for one cause while the key is the cause. + */ + public var propertyName: String { + switch self { + case let .setPaint(_, key, _, _), let .setLayout(_, key, _, _): return key + case .setFilter: return "filter" + case .setZoomRange: return "zoom range" + case .addLayer, .removeLayer: return "layer" + } + } + + /// The mutation that puts back what was there, or nil when there is nothing to put back. + public func reversed() -> StyleMutation? { + switch self { + case let .setPaint(layerId, key, value, previous): + return .setPaint(layerId: layerId, key: key, value: previous, previous: value) + case let .setLayout(layerId, key, value, previous): + return .setLayout(layerId: layerId, key: key, value: previous, previous: value) + case let .setFilter(layerId, value, previous): + return .setFilter(layerId: layerId, value: previous, previous: value) + case let .setZoomRange(layerId, minZoom, maxZoom, previousMin, previousMax): + return .setZoomRange( + layerId: layerId, + minZoom: previousMin, + maxZoom: previousMax, + previousMinZoom: minZoom, + previousMaxZoom: maxZoom + ) + case let .addLayer(layerId, _, _): + return .removeLayer(layerId: layerId) + case .removeLayer: + return nil + } + } + + /** + Reads the compiler's `mutations` array. + + An entry this version does not know is skipped rather than fatal: the + array is produced by the native library this host shipped with, so an + unknown op means the two went out of step, and dropping one change is + better than losing every change. + */ + public static func parseList(_ json: String) -> [StyleMutation] { + guard + let data = json.data(using: .utf8), + let array = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] + else { return [] } + return array.compactMap(parse) + } + + private static func parse(_ obj: [String: Any]) -> StyleMutation? { + let layerId = obj["layerId"] as? String + switch obj["op"] as? String { + case "setPaint": + guard let layerId, let key = obj["key"] as? String else { return nil } + return .setPaint( + layerId: layerId, key: key, + value: jsonText(of: obj["value"]), previous: jsonText(of: obj["previous"]) + ) + case "setLayout": + guard let layerId, let key = obj["key"] as? String else { return nil } + return .setLayout( + layerId: layerId, key: key, + value: jsonText(of: obj["value"]), previous: jsonText(of: obj["previous"]) + ) + case "setFilter": + guard let layerId else { return nil } + return .setFilter( + layerId: layerId, + value: jsonText(of: obj["value"]), previous: jsonText(of: obj["previous"]) + ) + case "setZoomRange": + guard let layerId else { return nil } + return .setZoomRange( + layerId: layerId, + minZoom: obj["minZoom"] as? Double, + maxZoom: obj["maxZoom"] as? Double, + previousMinZoom: obj["previousMinZoom"] as? Double, + previousMaxZoom: obj["previousMaxZoom"] as? Double + ) + case "addLayer": + guard let layerId, obj["layer"] is [String: Any] else { return nil } + return .addLayer( + layerId: layerId, layer: jsonText(of: obj["layer"]), + beforeId: obj["beforeId"] as? String + ) + case "removeLayer": + return layerId.map { .removeLayer(layerId: $0) } + default: + return nil + } + } + + + /** + A JSON node as the text that would produce it. + + `JSONSerialization` refuses a bare string or number as a top-level + value on older systems, so the simple cases are written directly rather + than asking it. + */ + public static func jsonText(of value: Any?) -> String { + switch value { + case nil, is NSNull: + return "null" + case let text as String: + let escaped = text + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\t", with: "\\t") + return "\"\(escaped)\"" + case let flag as Bool where type(of: value!) == type(of: NSNumber(value: true)): + return flag ? "true" : "false" + case let number as NSNumber: + // `true` and `false` arrive as NSNumber too; the case above + // catches them by their boxed type. + if CFGetTypeID(number) == CFBooleanGetTypeID() { + return number.boolValue ? "true" : "false" + } + return number.stringValue + default: + guard + let value, + JSONSerialization.isValidJSONObject(value), + let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]), + let text = String(data: data, encoding: .utf8) + else { return "null" } + return text + } + } +} diff --git a/Sources/MapConductorCore/map/VectorStyleMutationSupport.swift b/Sources/MapConductorCore/map/VectorStyleMutationSupport.swift new file mode 100644 index 0000000..78c4135 --- /dev/null +++ b/Sources/MapConductorCore/map/VectorStyleMutationSupport.swift @@ -0,0 +1,117 @@ +import Foundation + +/** + A map that can be told to change a loaded style in place. + + Separate from ``VectorStyleSupport``, which is about making a style *be* the + basemap. This one is about changing the one that is already up, without the + reload that would otherwise cost every tile on screen and every overlay the + app added. A provider declares it under ``VectorStyleMutationSupportKey``. + + Not every provider can take every change: MapLibre here exposes its paint + properties as typed `NSExpression`s rather than by name, so a raw style-spec + key it has no mapping for cannot be set. That is why ``apply(_:)`` answers + with what it could not do instead of returning nothing -- a caller that + silently assumed success would show a map that is half adjusted and say + nothing about it. + + android-sdk-core and js-sdk-core carry the same capability. + */ +public protocol VectorStyleMutationSupport: AnyObject { + /** + Applies `mutations` to the live style, replacing any set applied before: + anything the previous set changed and this one does not is put back the + way it was. + + - Returns: the mutations that could not be applied, empty when all were. + */ + func apply(_ mutations: [StyleMutation]) -> [StyleMutation] + + /// Puts back everything ``apply(_:)`` changed. + func clear() +} + +/// ``VectorStyleMutationSupport`` の登録キー。 +/// +/// 宣言しないプロバイダは「生きているスタイルは触れない」。スタイル調整は +/// その場合、文書を差し替えるか(地図が読み直す)、ラスター経路で描き直す。 +public enum VectorStyleMutationSupportKey: MapServiceKey { + public typealias Value = VectorStyleMutationSupport +} + +/** + The bookkeeping every provider's ``VectorStyleMutationSupport`` needs, so no + provider has to get it right twice. + + Two things are easy to miss and both show up as "the adjustment worked, and + then stopped": + + - **Replacing a set has to undo what it drops.** Moving a slider sends a + whole new set each time. A property the new set no longer mentions must go + back to the value the style gave it -- and only the mutation knows that + value, because by now the renderer's own idea of it is the one this put + there. + - **A style load wipes everything.** The app switching basemap, the provider + reloading, a design change: whatever the reason, the renderer comes back + with the document's own values and nothing this applied. The provider + calls ``onStyleLoaded()`` and the whole set goes on again. + + - Parameter applyNow: puts mutations on the live style and returns the ones + it could not. Called on whatever thread ``apply(_:)`` was called on; a + provider whose renderer is main-thread-only hops there itself. + */ +public final class StyleMutationStore: VectorStyleMutationSupport, @unchecked Sendable { + private let lock = NSLock() + private let applyNow: ([StyleMutation]) -> [StyleMutation] + private var current: [StyleMutation] = [] + + public init(applyNow: @escaping ([StyleMutation]) -> [StyleMutation]) { + self.applyNow = applyNow + } + + public func apply(_ mutations: [StyleMutation]) -> [StyleMutation] { + lock.lock() + let targets = Set(mutations.map(\.target)) + let restores = current.filter { !targets.contains($0.target) }.compactMap { $0.reversed() } + current = mutations + lock.unlock() + + // Restores first: a property moving from one rule's value to no rule + // at all must pass through the style's own value, not stay on the old + // one because the new set happened to be applied first. + // + // Nothing is logged from here. What could not be applied is the + // caller's to report -- it has the app's diagnostics callback. + return applyNow(restores + mutations) + } + + public func clear() { + lock.lock() + let restores = current.compactMap { $0.reversed() } + current = [] + lock.unlock() + if !restores.isEmpty { _ = applyNow(restores) } + } + + /** + Re-applies everything after the map has loaded a style. + + The provider calls this from whatever it has that fires on a style being + ready. Without it an adjustment survives only until the next design + change, which is the kind of bug that is found weeks later by someone + switching basemap with a slider open. + */ + public func onStyleLoaded() { + lock.lock() + let again = current + lock.unlock() + if !again.isEmpty { _ = applyNow(again) } + } + + /// What is applied right now. For tests and diagnostics. + public var applied: [StyleMutation] { + lock.lock() + defer { lock.unlock() } + return current + } +} diff --git a/Sources/MapConductorCore/map/VectorStyleSupport.swift b/Sources/MapConductorCore/map/VectorStyleSupport.swift new file mode 100644 index 0000000..afb7022 --- /dev/null +++ b/Sources/MapConductorCore/map/VectorStyleSupport.swift @@ -0,0 +1,169 @@ +import Foundation + +/** + A map that can draw a MapLibre style itself. + + MapLibre, Mapbox and MapTiler are vector renderers already: handing them a + style document is cheaper than rasterising it on the device and feeding the + result back as PNG tiles, which is what a backend that only takes raster + (Google Maps, MapKit, ArcGIS, HERE...) has to be given. A provider that can + take the style directly registers this under ``VectorStyleSupportKey``; a + layer that has a style to show looks the key up and, if it is there, skips + the rasteriser altogether. + + The style *becomes the basemap*: on every provider that can do this, the + style is the map, so showing one replaces the design the state names and + clearing it puts that design back. A style drawn this way is not an overlay + -- it has no opacity, and nothing of the previous basemap shows through it. + Anything that needs the style on top of another basemap goes through the + raster path. + + android-sdk-core and js-sdk-core carry the same capability. + */ +public protocol VectorStyleSupport: AnyObject { + /** + Draws the style at `url` as the map's basemap. + + `attributionRules` are the credits the style's sources ask for, carried + by the design so the map's attribution overlay shows them for as long as + the style is up. + */ + func showStyle(url: String, attributionRules: [AttributionRule]) + + /** + Restores the design that was showing before ``showStyle(url:attributionRules:)``, + unless the app has since chosen another one, in which case that choice + is left alone. + */ + func clearStyle() + + /** + Where the style the map is drawing came from, when that is knowable. + + This is what lets a rule set adjust *whatever is showing* rather than a + style the app had to name: the document has to be read before a rule can + be matched against its layers, and only the provider knows where it came + from. + + Nil when it is not a URL anything can fetch -- a provider's own named + design (`mapbox://styles/mapbox/standard`), a style that needs a key + this does not have. A caller that wanted it says so and leaves the map + alone rather than showing something unstyled. + */ + var currentStyleUrl: String? { get } +} + +public extension VectorStyleSupport { + var currentStyleUrl: String? { nil } +} + +/// ``VectorStyleSupport`` の登録キー。 +/// +/// 宣言しないプロバイダは「スタイルは受け取れない」。ベクタータイル層はその場合 +/// ラスタータイルに描いて渡す。 +public enum VectorStyleSupportKey: MapServiceKey { + public typealias Value = VectorStyleSupport +} + +/** + ``VectorStyleSupport`` for a provider whose design type can name a style URL. + + The three MapLibre-based providers differ only in how a URL is wrapped into + their design type, which is what `designFor` supplies; the remember-and- + restore dance is the same for all of them and lives here once. The "still + ours" check on ``clearStyle()`` is what keeps this from fighting an app that + switched designs while the style was up: a layer unmounting must not undo a + choice the app made after it. + + ``clearStyle()`` takes effect a moment later rather than at once, and a + ``showStyle(url:attributionRules:)`` of the same URL in between cancels it. + A host that rebuilds its overlay content on a design change remounts the + layer that asked for the style *by the change it caused*; clearing on that + unmount would restore the old design, which would rebuild again, and so on + without end. Waiting one frame lets the remount cancel the clear. + */ +public final class VectorStyleAsDesign: VectorStyleSupport { + private weak var state: State? + private let designId: (State.ActualMapDesignType) -> String + private let designFor: (String, [AttributionRule]) -> State.ActualMapDesignType + private let urlOf: ((State.ActualMapDesignType) -> String?)? + + private var installed: State.ActualMapDesignType? + private var installedUrl: String? + private var previous: State.ActualMapDesignType? + private var pendingClear: DispatchWorkItem? + + public init( + state: State, + designId: @escaping (State.ActualMapDesignType) -> String, + urlOf: ((State.ActualMapDesignType) -> String?)? = nil, + designFor: @escaping (_ url: String, _ attributionRules: [AttributionRule]) -> State.ActualMapDesignType + ) { + self.state = state + self.designId = designId + self.urlOf = urlOf + self.designFor = designFor + } + + /** + Where the design **the app chose** points, when it points anywhere + fetchable. + + Deliberately not the URL this capability installed. That one is the + document the style module served a moment ago, and it is unregistered + from the tile server the instant the style comes off -- so answering + with it turns "adjust whatever is showing" into a 404 against our own + corpse. When one of ours is up, the honest answer is the design it + replaced. + + Nil unless the provider supplied `urlOf`, and nil from that for a design + that is not a plain style document -- a provider's own named basemap, or + one that needs a key this does not have. + */ + public var currentStyleUrl: String? { + guard let urlOf else { return nil } + if installed != nil { return previous.flatMap(urlOf) } + guard let state else { return nil } + return urlOf(state.mapDesignType) + } + + public func showStyle(url: String, attributionRules: [AttributionRule]) { + pendingClear?.cancel() + pendingClear = nil + guard let state else { return } + // Same style, still up: nothing to do, and writing the design again + // would reload it for nothing. + if let current = installed, installedUrl == url, designId(state.mapDesignType) == designId(current) { + return + } + let design = designFor(url, attributionRules) + // Showing a second style over the first keeps the *original* design + // as the one to go back to; the intermediate style was never the + // app's basemap. + if installed == nil { previous = state.mapDesignType } + installed = design + installedUrl = url + state.mapDesignType = design + } + + public func clearStyle() { + guard installed != nil else { return } + pendingClear?.cancel() + let clear = DispatchWorkItem { [weak self] in + guard let self else { return } + self.pendingClear = nil + guard let current = self.installed else { return } + let restore = self.previous + self.installed = nil + self.installedUrl = nil + self.previous = nil + guard let state = self.state, let restore else { return } + if self.designId(state.mapDesignType) == self.designId(current) { + state.mapDesignType = restore + } + } + pendingClear = clear + // Longer than a frame, shorter than anyone notices. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: clear) + } +} diff --git a/Sources/MapConductorCore/marker/AbstractMarkerController.swift b/Sources/MapConductorCore/marker/AbstractMarkerController.swift index da89c7b..f1714b2 100644 --- a/Sources/MapConductorCore/marker/AbstractMarkerController.swift +++ b/Sources/MapConductorCore/marker/AbstractMarkerController.swift @@ -123,6 +123,16 @@ open class AbstractMarkerController< open func add(data: [MarkerState]) async { if markerManager.isDestroyed { return } MCLog.marker("AbstractMarkerController.add count=\(data.count)") + let addStart = DispatchTime.now().uptimeNanoseconds + defer { + let addMs = Double(DispatchTime.now().uptimeNanoseconds - addStart) / 1e6 + if addMs > 100 { + // メインアクター上で走るので、この時間はそのまま UI が止まった + // 時間になり得る。ピンチ中に出続けるなら、上流が同じ一覧を + // 送り直している(syncMarkers の fast path が効いていない)。 + MCLog.probe("SLOW markerController.add \(Int(addMs))ms count=\(data.count)") + } + } let entitiesToAnimate: [MarkerEntity] = await semaphore.withPermit { if markerManager.isDestroyed { return [] } var modifiedEntities: [MarkerEntity] = [] diff --git a/Sources/MapConductorCore/marker/MarkerGrid.swift b/Sources/MapConductorCore/marker/MarkerGrid.swift new file mode 100644 index 0000000..ecd6820 --- /dev/null +++ b/Sources/MapConductorCore/marker/MarkerGrid.swift @@ -0,0 +1,245 @@ +import Foundation + +/// The cell arithmetic behind `MarkerGridIndex`, with no marker in sight. +/// +/// Kept apart from the index for two reasons. The index is generic over the +/// marker type, and Swift will not let a generic type hold a stored static, so +/// every constant here would have had to be a computed property returning a +/// literal. More to the point, none of this depends on what a marker is: it is +/// a way of naming a patch of the globe, and the conformance fixtures the three +/// SDKs share test it on its own. +/// +/// ## The scheme +/// +/// The world starts as **two square cells** — the western and eastern halves, +/// 180 degrees on a side — and each is halved in latitude and in longitude, +/// repeatedly. The key records the half chosen each time, latitude bit above +/// longitude bit, under the bit that says which hemisphere it started in. That +/// makes **every prefix of a key a cell:** dropping the low `2n` bits names the +/// cell `n` levels up, and all of its children share the prefix, so they form +/// one run in a sorted array. A query picks the level that suits its box +/// instead of paying the finest one. +/// +/// ## Why two root cells and not one +/// +/// One root cell covering the whole globe is what cordova-plugin-googlemaps' +/// `geomodel.js` uses, and it is what this started as. It makes every cell +/// **twice as wide as it is tall**, because the same number of divisions covers +/// 360 degrees of longitude and 180 of latitude. +/// +/// That costs real time. A query asks for cells no wider than some separation; +/// with a 2:1 cell, satisfying the width over-resolves the height by a factor +/// of two, so the query walks twice the cells and returns twice the markers it +/// asked for. Measured on Tokyo's 144,183 street trees at zoom 11, a tile +/// returned 9,715 markers of which the caller kept 2,156, and the work that +/// scales with that count — the walk, positioning each marker, and grouping +/// them — was 44 ms of the tile's 52 ms. +/// +/// Splitting the root into two square halves removes the factor of two. The +/// string form keeps its shape: a hemisphere digit followed by geocell +/// characters over `0123456789abcdef`, one per 4x4 subdivision, and a prefix is +/// still a cell. What it is no longer is byte-identical to `geomodel.js`. +/// +/// The three SDKs hold the key as a `Long`, an `Int64` and a `number`; the +/// string is what their conformance fixtures compare, because it does not +/// depend on that choice. +enum MarkerGrid { + + /// Levels of 2x2 subdivision below the two root cells. + /// + /// 18 is the largest depth that still leaves room for the position: the key + /// is `2 * 18 + 1 = 37` bits — the extra one is the hemisphere — and the + /// position below it is 24, which is 61 of the 63 an `Int64` has. It is + /// also an even number of levels, so the string form packs into whole + /// 4-bit characters with no ragged tail. + /// + /// A cell at the bottom is 0.000687 degrees on a side, about 76 m at the + /// equator. The flat grid this replaced was 0.005 degrees. + static var gridDepth: Int64 { 18 } + + /// Roughly how many cells across a box a query aims to walk. + /// + /// The level is chosen so the box covers about this many cells per axis, + /// which bounds the walk at `cellsAcross^2` lookups no matter how large the + /// box is. That is what removed the old "past 4096 cells, scan everything + /// instead" escape: a world-sized box now asks a coarse level and reads + /// roughly the markers it would have scanned anyway, without the cliff. + static var cellsAcross: Double { 32 } + + /// The most cells a query walks along either axis. + /// + /// Only bites on a box so flat or so narrow that its area says nothing + /// about how many cells it crosses. + static var maxCellsPerAxis: Double { 64 } + + // MARK: - Cells + + /// Degrees a cell spans, on either axis. Cells are square. + static func cellSize(_ level: Int64) -> Double { 180.0 / Double(Int64(1) << level) } + + /// Rows of cells from pole to pole at `level`. + static func rows(_ level: Int64) -> Int64 { Int64(1) << level } + + /// Columns of cells around the globe at `level`. Twice the rows, because + /// the world is twice as wide as it is tall and the cells are square. + static func columns(_ level: Int64) -> Int64 { Int64(1) << (level + 1) } + + /// The coarsest level whose cells are no wider than `separation`, or nil if + /// even the bottom level is coarser than that. + /// + /// Counted up rather than solved with a logarithm, because the three SDKs + /// have to agree on the answer and they do not have the same `log2`. Java + /// has none at all — Kotlin computes `ln(x) / ln(2)`, which is off by an ulp + /// where the true answer is a whole number, and the `ceil` above it turns + /// that ulp into a different level. Halving a double and comparing it is + /// exact on all three. + static func level(forSeparation separation: Double) -> Int64? { + guard separation > 0 else { return nil } + var level: Int64 = 0 + while level <= gridDepth { + if cellSize(level) <= separation { return level } + level += 1 + } + return nil + } + + /// The level a bounds query walks, chosen so the box covers about + /// `cellsAcross` squared cells. + /// + /// Counted up for the same reason as `level(forSeparation:)`. + /// + /// The area rule alone is not enough. A box with no height — a click box + /// flattened by a degenerate projection, a bounds built from two points on + /// the same parallel — has an area of nothing, which asks for the bottom + /// level, which is a quarter of a million columns to walk. `maxCellsPerAxis` + /// steps back up until neither axis is absurd. On a box of ordinary shape + /// the area rule binds first and this does nothing. + static func queryLevel(latSpan: Double, lonSpan: Double) -> Int64 { + let area = max(lonSpan, 1e-12) * max(latSpan, 1e-12) + // cells at a level = area / cellSize^2, and cellSize = 180 / 2^level + let budget = cellsAcross * cellsAcross * 180.0 * 180.0 + var level: Int64 = 0 + while level < gridDepth, area * Double(Int64(1) << (2 * (level + 1))) <= budget { + level += 1 + } + while level > 0, + lonSpan / cellSize(level) > maxCellsPerAxis + || latSpan / cellSize(level) > maxCellsPerAxis { + level -= 1 + } + return level + } + + /// How far east the box runs, from its west edge to its east one. + /// + /// A box crossing the antimeridian has its east corner west of its west + /// one, and a padded box can run past ±180 outright. Normalising to a + /// single eastward span removes both cases: everything downstream walks + /// east from the west edge for this many degrees, and never compares two + /// longitudes. + static func eastwardSpan(from west: Double, to east: Double) -> Double { + if east - west >= 360.0 { return 360.0 } + return (((east - west).truncatingRemainder(dividingBy: 360.0)) + 360.0) + .truncatingRemainder(dividingBy: 360.0) + } + + /// The columns a box covers: where to start, and how many to walk. + /// + /// The end column is taken from `west + span`, not from the east corner. + /// Taking it from the corner cannot work once column indices fold: the + /// column holding 180 and the column holding -180 are the same one, so a + /// box from -180.2 to 179.8 starts and ends on the same column and the walk + /// covers one column out of the level's many. Nothing errors; the query + /// simply answers for a thin slice of the world. + /// + /// The old row-major key did not have this hazard, because it indexed on + /// unwrapped column numbers that kept growing eastward. Folding is what + /// makes a Morton key a fixed width, so the walk has to carry the span + /// itself. + static func columnWalk( + from west: Double, + spanning span: Double, + level: Int64 + ) -> (start: Int64, count: Int64) { + let total = columns(level) + let start = Int64(((west + 180.0) / 360.0 * Double(total)).rounded(.down)) + let end = Int64(((west + span + 180.0) / 360.0 * Double(total)).rounded(.down)) + // A full turn ends on the column it started on; walking both would + // return that column's markers twice. + return (start, min(end - start + 1, total)) + } + + static func latCell(_ latitude: Double, level: Int64) -> Int64 { + let count = rows(level) + let at = Int64(((latitude + 90.0) / 180.0 * Double(count)).rounded(.down)) + return min(max(at, 0), count - 1) + } + + static func lonCell(_ longitude: Double, level: Int64) -> Int64 { + let count = columns(level) + let at = Int64(((longitude + 180.0) / 360.0 * Double(count)).rounded(.down)) + return wrap(at, level: level) + } + + static func wrap(_ lonCell: Int64, level: Int64) -> Int64 { + let count = columns(level) + return ((lonCell % count) + count) % count + } + + static func mortonKey(for position: GeoPointProtocol) -> Int64 { + morton( + latCell(position.latitude, level: gridDepth), + lonCell(position.longitude, level: gridDepth), + level: gridDepth + ) + } + + /// Interleaves the two indices, latitude in the odd bits, under the bit + /// that says which root cell they are in. + /// + /// Longitude carries one bit more than latitude — twice as many columns as + /// rows — and that top bit is the hemisphere. It sits above the interleaved + /// pairs rather than inside them, so dropping the low two bits still names + /// the parent cell. + static func morton(_ latCell: Int64, _ lonCell: Int64, level: Int64) -> Int64 { + let hemisphere = lonCell >> level + let within = lonCell & ((Int64(1) << level) - 1) + return (hemisphere << (2 * level)) | (spread(latCell) << 1) | spread(within) + } + + /// Spreads a value's bits apart, leaving a zero between each pair. + private static func spread(_ value: Int64) -> Int64 { + var x = UInt64(bitPattern: value) & 0xFFFF_FFFF + x = (x | (x << 16)) & 0x0000_FFFF_0000_FFFF + x = (x | (x << 8)) & 0x00FF_00FF_00FF_00FF + x = (x | (x << 4)) & 0x0F0F_0F0F_0F0F_0F0F + x = (x | (x << 2)) & 0x3333_3333_3333_3333 + x = (x | (x << 1)) & 0x5555_5555_5555_5555 + return Int64(bitPattern: x) + } + + /// The cell's name as a string: a hemisphere digit, then one character per + /// 4x4 subdivision over `0123456789abcdef`. + /// + /// Each character after the first is two levels of this grid, so a string of + /// `characters` names the cell at level `2 * (characters - 1)`. Not used by + /// the index itself — the key is the same information, and faster — but it + /// is the form the three SDKs' conformance fixtures compare, because it does + /// not depend on how a platform stores an integer. + static func geocell(latitude: Double, longitude: Double, characters: Int) -> String { + let alphabet = Array("0123456789abcdef") + let level = min(Int64(max(characters - 1, 0)) * 2, gridDepth) + let key = morton( + latCell(latitude, level: level), + lonCell(longitude, level: level), + level: level + ) + var text = String(key >> (2 * level)) + var at = level * 2 - 4 + while at >= 0 { + text.append(alphabet[Int((key >> at) & 0xF)]) + at -= 4 + } + return text + } +} diff --git a/Sources/MapConductorCore/marker/MarkerGridIndex.swift b/Sources/MapConductorCore/marker/MarkerGridIndex.swift new file mode 100644 index 0000000..f88dd0d --- /dev/null +++ b/Sources/MapConductorCore/marker/MarkerGridIndex.swift @@ -0,0 +1,303 @@ +import Foundation + +/// A hierarchical lat/lng grid over the markers, held as one sorted array of Int64. +/// +/// Replaces the hex-cell registry on the paths that run per tile. That index was +/// built at a fixed zoom of 20, which puts a cell at about 12 cm — street trees +/// are metres apart, so every marker landed in its own cell and the index +/// grouped nothing, while costing a HexCell and a String id per marker plus a +/// kd-tree node each. On Tokyo's 144,183 street trees, building it on Android +/// aborted the process; this builds in 41 ms and 1.15 MB there. +/// +/// Each marker is one Int64: its cell key above its position in a snapshot, so +/// building the index is arithmetic and a sort of a contiguous buffer — no +/// objects, no hashing, no strings. +/// +/// The key itself is a Morton code over a hierarchy of cells, described in +/// `MarkerGrid`. What it buys the index is that a cell at *any* level is one +/// contiguous run, so a query walks the level that suits its box rather than +/// the finest one — the first version had a single fixed cell size of 0.005 +/// degrees, and a continent-wide box paid for it. +/// +/// Not synchronised. `MarkerManager` holds its lock across every call, and +/// taking a second one here would only add cost to a path that is already +/// serialised. +/// +/// The Android and web SDKs index the same way, to the same depth. +final class MarkerGridIndex { + + /// 24 bits of position, enough for 16.7M markers, under the cell key. + private static var indexBits: Int64 { 24 } + private static var indexMask: Int64 { (1 << 24) - 1 } + + /// Past this many cells, decline the thinned query. + /// + /// Thinning cannot choose its own level — the caller's separation fixes it — + /// so this bound stays. Same value as android's + /// `MAX_CELLS_PER_THINNED_QUERY`. + private static var maxCellsPerThinnedQuery: Int64 { 1 << 18 } + + /// The level `nearest` walks its rings at, and the reach that buys. + /// + /// Level 15 is 0.0055 degrees on a side, matching the flat cell this index + /// used to have, so 100 rings is about 45 km as before. + private static var nearestLevel: Int64 { 15 } + private static var maxNearestRings: Int64 { 100 } + + private let source: () -> [MarkerEntity] + private var snapshot: [MarkerEntity] = [] + /// Sorted `(mortonKey << indexBits) | position-in-snapshot`. + private var packed: [Int64] = [] + private var dirty = true + + init(source: @escaping () -> [MarkerEntity]) { + self.source = source + } + + /// Whether the index currently holds a built snapshot. + var isBuilt: Bool { !dirty } + + /// Roughly what the index costs: two words a marker, key and reference. + func estimatedBytes() -> Int64 { Int64(packed.count) * 8 + Int64(snapshot.count) * 8 } + + /// Marks the index stale. The rebuild happens on the next query. + func invalidate() { + dirty = true + } + + /// Markers whose position falls inside `bounds`. + func inBounds(_ bounds: GeoRectBounds) -> [MarkerEntity] { + guard let southWest = bounds.southWest, let northEast = bounds.northEast else { return [] } + + // A box crossing the antimeridian has its east corner west of its west + // one. Walking to the unwrapped end and folding each column back onto + // the globe covers both halves without a second loop — and without the + // `lonFrom...lonTo` that traps on a reversed range. + let lonSpan = MarkerGrid.eastwardSpan(from: southWest.longitude, to: northEast.longitude) + let level = MarkerGrid.queryLevel( + latSpan: northEast.latitude - southWest.latitude, + lonSpan: lonSpan + ) + let latFrom = MarkerGrid.latCell(southWest.latitude, level: level) + let latTo = MarkerGrid.latCell(northEast.latitude, level: level) + if latTo < latFrom { return [] } + let columns = MarkerGrid.columnWalk(from: southWest.longitude, spanning: lonSpan, level: level) + + rebuildIfNeeded() + var found: [MarkerEntity] = [] + for latCell in latFrom...latTo { + for step in 0.. [MarkerEntity]? { + guard let level = MarkerGrid.level(forSeparation: minSeparationDegrees) else { return nil } + guard let southWest = bounds.southWest, let northEast = bounds.northEast else { return nil } + + let lonSpan = MarkerGrid.eastwardSpan(from: southWest.longitude, to: northEast.longitude) + let latFrom = MarkerGrid.latCell(southWest.latitude, level: level) + let latTo = MarkerGrid.latCell(northEast.latitude, level: level) + if latTo < latFrom { return [] } + let columns = MarkerGrid.columnWalk(from: southWest.longitude, spanning: lonSpan, level: level) + + if (latTo - latFrom + 1) * columns.count > Self.maxCellsPerThinnedQuery { return nil } + + rebuildIfNeeded() + let shift = Self.indexBits + 2 * (MarkerGrid.gridDepth - level) + var found: [MarkerEntity] = [] + for latCell in latFrom...latTo { + for step in 0..= packed.count { continue } + let limit = (key + 1) << shift + if packed[at] >= limit { continue } + // The run's last entry. No containment test anywhere: that is + // the whole point, and it is also why this is cheaper than the + // version that scanned every border cell. + var last = at + while last + 1 < packed.count && packed[last + 1] < limit { last += 1 } + found.append(snapshot[Int(packed[last] & Self.indexMask)]) + } + } + return found + } + + /// The marker nearest `position`, by squared degrees. + /// + /// Rings of cells are searched outward from the one holding the point. The + /// stopping rule is a distance, not a ring count: a ring is a square, so a + /// hit in one of its corners sits about 1.4 cells further out than a hit on + /// its edge, and stopping a fixed ring after the first hit returns the + /// wrong marker. Having finished ring r, everything still unsearched is at + /// least r cells away, so the search ends once that already exceeds the + /// best distance found. + func nearest(position: GeoPointProtocol) -> MarkerEntity? { + rebuildIfNeeded() + if packed.isEmpty { return nil } + + let level = Self.nearestLevel + let centreLat = MarkerGrid.latCell(position.latitude, level: level) + let centreLon = MarkerGrid.lonCell(position.longitude, level: level) + + var best: MarkerEntity? + var bestDistance = Double.greatestFiniteMagnitude + var ring: Int64 = 0 + + while ring <= Self.maxNearestRings { + forEach(inRing: ring, centreLat: centreLat, centreLon: centreLon, level: level) { entity in + let distance = Self.squaredDegrees(entity, position) + if distance < bestDistance { + bestDistance = distance + best = entity + } + } + // A ring is a square: the nearest unsearched point is `ring` cells + // away on the shorter axis, which is latitude. + let reach = Double(ring) * MarkerGrid.cellSize(level) + if best != nil, reach * reach >= bestDistance { break } + ring += 1 + } + + // Nothing within the rings searched: the set is sparse enough here that + // scanning is cheaper than widening further. + return best ?? source().min { Self.squaredDegrees($0, position) < Self.squaredDegrees($1, position) } + } + + private static func squaredDegrees( + _ entity: MarkerEntity, + _ position: GeoPointProtocol + ) -> Double { + let deltaLat = entity.state.position.latitude - position.latitude + let deltaLon = entity.state.position.longitude - position.longitude + return deltaLat * deltaLat + deltaLon * deltaLon + } + + private func forEach( + inRing ring: Int64, + centreLat: Int64, + centreLon: Int64, + level: Int64, + _ body: (MarkerEntity) -> Void + ) { + func visit(_ latCell: Int64, _ lonCell: Int64) { + let rows = MarkerGrid.rows(level) + if latCell < 0 || latCell >= rows { return } + forEach(inCell: MarkerGrid.morton(latCell, MarkerGrid.wrap(lonCell, level: level), level: level), + level: level, body) + } + if ring == 0 { + visit(centreLat, centreLon) + return + } + for offset in -ring...ring { + visit(centreLat - ring, centreLon + offset) + visit(centreLat + ring, centreLon + offset) + } + if ring > 1 { + for offset in (-ring + 1)...(ring - 1) { + visit(centreLat + offset, centreLon - ring) + visit(centreLat + offset, centreLon + ring) + } + } + } + + /// Walks one cell at `level`. Its markers are the run whose keys share the + /// cell's prefix, which is what interleaving the bits bought. + private func forEach( + inCell key: Int64, + level: Int64, + _ body: (MarkerEntity) -> Void + ) { + let shift = Self.indexBits + 2 * (MarkerGrid.gridDepth - level) + var at = lowerBound(key << shift) + let limit = (key + 1) << shift + while at < packed.count, packed[at] < limit { + body(snapshot[Int(packed[at] & Self.indexMask)]) + at += 1 + } + } + + private func rebuildIfNeeded() { + guard dirty else { return } + let entities = source() + var keys = [Int64]() + keys.reserveCapacity(entities.count) + for (at, entity) in entities.enumerated() { + keys.append((MarkerGrid.mortonKey(for: entity.state.position) << Self.indexBits) | Int64(at)) + } + keys.sort() + snapshot = entities + packed = keys + dirty = false + } + + private func lowerBound(_ target: Int64) -> Int { + var low = 0 + var high = packed.count + while low < high { + let mid = (low + high) / 2 + if packed[mid] < target { low = mid + 1 } else { high = mid } + } + return low + } +} diff --git a/Sources/MapConductorCore/marker/MarkerListIdentity.swift b/Sources/MapConductorCore/marker/MarkerListIdentity.swift new file mode 100644 index 0000000..ec43d8f --- /dev/null +++ b/Sources/MapConductorCore/marker/MarkerListIdentity.swift @@ -0,0 +1,45 @@ +import Foundation + +/// 「同じマーカー一覧の再送」を入口で見抜く門番。 +/// +/// SwiftUI はカメラが動くたびに map view の body を再評価し、そのたびに +/// `updateContent` が各プロバイダの `syncMarkers` へ**全マーカー**を運んでくる。 +/// ピンチ中はそれが連続する。一覧そのものは変わっていないのに、各コントローラは +/// 毎回 id の Set を 2 つ作り、辞書を組み直し、`hasEntity` をマーカーの数だけ +/// 呼んでいた(= ロックをマーカーの数だけ取っていた)。東京の街路樹 144,183 本 +/// では 1 回あたり数百 ms がメインスレッドに乗り、ピンチが 0.3〜0.8 秒刻みで +/// 凍る -- 実機の画面収録で操作時間の 89% が凍結だった。Android は camera の +/// 変化で content の再構築が走らないため、この経路自体が無い。iOS だけ重い、 +/// の実体がこれ。 +/// +/// 比べるのは**参照だけ**。順序込みの同一性が一致すれば、購読も辞書も既に +/// 正しく、位置などの変化は各 state の購読が届ける。O(n) だがアロケーションが +/// 無いので 144k 件で 1ms 未満。9 本のプロバイダコントローラがすべて同じ +/// `syncMarkers` の形を持つので、判定だけをここに共通化する。 +/// +/// スレッドは呼び出し側(メインアクター)に従う。自前のロックは持たない。 +@_spi(MapConductorDriver) +public struct MarkerListIdentity { + private var lastSynced: [ObjectIdentifier] = [] + + public init() {} + + /// 一覧が前回と同一(同じ state インスタンスが同じ順で同じ数)なら false。 + /// 変わっていれば覚え直して true。 + /// + /// 名前は「処理すべきか」。false のときに呼び出し側がやってよいのは、 + /// 一覧と無関係な O(1) の後始末(サーバ再起動の検知など)だけ。 + public mutating func shouldProcess(_ markers: [Marker]) -> Bool { + if markers.count == lastSynced.count, !markers.isEmpty { + var identical = true + for (at, marker) in markers.enumerated() + where lastSynced[at] != ObjectIdentifier(marker.state) { + identical = false + break + } + if identical { return false } + } + lastSynced = markers.map { ObjectIdentifier($0.state) } + return true + } +} diff --git a/Sources/MapConductorCore/marker/MarkerManager.swift b/Sources/MapConductorCore/marker/MarkerManager.swift index 8ebe430..02ee9cb 100644 --- a/Sources/MapConductorCore/marker/MarkerManager.swift +++ b/Sources/MapConductorCore/marker/MarkerManager.swift @@ -11,7 +11,15 @@ public final class MarkerManager { public let minMarkerCount: Int private var entities: [String: MarkerEntity] = [:] + /// Only `findByIdPrefix` still needs hex cells; it is built the first time + /// that is called, so nothing else pays for it. private var cellRegistry: HexCellRegistry? + /// The index the per-tile queries use. Weak, because the manager owns the + /// markers and the index only ever borrows them. + private lazy var gridIndex = MarkerGridIndex { [weak self] in + guard let self else { return [] } + return Array(self.entities.values) + } private var destroyed = false private let lock = NSLock() @@ -65,6 +73,7 @@ public final class MarkerManager { let removed = entities.removeValue(forKey: id) if let removed { cellRegistry?.removePoint(entity: removed) + gridIndex.invalidate() } return removed } @@ -75,6 +84,7 @@ public final class MarkerManager { guard usableLocked("registerEntity") else { return } entities[entity.state.id] = entity cellRegistry?.setPoint(entity: entity) + gridIndex.invalidate() } public func updateEntity(_ entity: MarkerEntity) { @@ -83,6 +93,7 @@ public final class MarkerManager { guard usableLocked("updateEntity") else { return } entities[entity.state.id] = entity cellRegistry?.setPoint(entity: entity) + gridIndex.invalidate() } public func metersPerPixel( @@ -105,20 +116,7 @@ public final class MarkerManager { guard usableLocked("findNearest") else { return nil } if entities.count > minMarkerCount { - let registry = ensureCellRegistryLocked() - if let nearestCell = registry.findNearest(point: position), - let ids = registry.getEntryIDsByHexCell(nearestCell) { - return ids - .compactMap { entities[$0] } - .min(by: { lhs, rhs in - let dx1 = lhs.state.position.latitude - position.latitude - let dy1 = lhs.state.position.longitude - position.longitude - let dx2 = rhs.state.position.latitude - position.latitude - let dy2 = rhs.state.position.longitude - position.longitude - return dx1 * dx1 + dy1 * dy1 < dx2 * dx2 + dy2 * dy2 - }) - } - return bruteForceNearestLocked(position: position) + return gridIndex.nearest(position: position) ?? bruteForceNearestLocked(position: position) } return bruteForceNearestLocked(position: position) @@ -138,7 +136,9 @@ public final class MarkerManager { lock.lock() defer { lock.unlock() } guard usableLocked("findByIdPrefix") else { return [] } - return cellRegistry?.findByIdPrefix(prefix) ?? [] + // The registry is built here and nowhere else: this is the one caller + // that needs hex cells rather than markers. + return ensureCellRegistryLocked().findByIdPrefix(prefix) } private func ensureCellRegistryLocked() -> HexCellRegistry { @@ -166,15 +166,8 @@ public final class MarkerManager { defer { lock.unlock() } guard usableLocked("findMarkersInBounds") else { return [] } - if entities.count > minMarkerCount, - let center = bounds.center, - let northEast = bounds.northEast { - let registry = ensureCellRegistryLocked() - let distance = Spherical.computeDistanceBetween(from: center, to: northEast) - let hexCells = registry.findWithinRadiusWithDistance(point: center, radius: distance) - let entryIDs = hexCells.compactMap { registry.getEntryIDsByHexCell($0.cell) } - .flatMap { $0 } - return entryIDs.compactMap { entities[$0] } + if entities.count > minMarkerCount { + return gridIndex.inBounds(bounds) } return entities.values.filter { entity in @@ -182,14 +175,42 @@ public final class MarkerManager { } } + /// The markers in `bounds`, thinned to at most one per + /// `minSeparationDegrees`. + /// + /// The caller is saying that markers closer than that are + /// interchangeable, which lets the index answer from its cells instead of + /// reading every marker. It declines — and this falls back to the full + /// query — when its cells are too coarse to honour the separation, so the + /// caller still has to apply the real rule to what comes back. + /// + /// Mirrors `findMarkersInBounds(bounds, minSeparationDegrees)` in + /// android-sdk. + public func findMarkersInBounds( + _ bounds: GeoRectBounds, + minSeparationDegrees: Double + ) -> [MarkerEntity] { + if bounds.isEmpty { return [] } + + lock.lock() + if usableLocked("findMarkersInBounds"), entities.count > minMarkerCount, + let thinned = gridIndex.inBoundsThinned(bounds, minSeparationDegrees: minSeparationDegrees) { + lock.unlock() + return thinned + } + lock.unlock() + + return findMarkersInBounds(bounds) + } + public func getMemoryStats() -> MarkerManagerStats { lock.lock() defer { lock.unlock() } _ = usableLocked("getMemoryStats") return MarkerManagerStats( entityCount: entities.count, - hasSpatialIndex: cellRegistry != nil, - spatialIndexInitialized: cellRegistry != nil, + hasSpatialIndex: true, + spatialIndexInitialized: gridIndex.isBuilt, estimatedMemoryKB: Int(estimateMemoryUsageLocked() / 1024) ) } @@ -197,7 +218,11 @@ public final class MarkerManager { private func estimateMemoryUsageLocked() -> Int64 { let entityMapOverhead = Int64(entities.count) * 64 let entityObjects = Int64(entities.count) * 200 - let spatialIndexSize = cellRegistry == nil ? 0 : Int64(entities.count) * 100 + let gridSize = gridIndex.estimatedBytes() + // The hex registry is usually absent; when findByIdPrefix has built it, + // it costs a cell object and a string id per marker. + let hexSize = cellRegistry == nil ? 0 : Int64(entities.count) * 100 + let spatialIndexSize = gridSize + hexSize return entityMapOverhead + entityObjects + spatialIndexSize } @@ -207,6 +232,7 @@ public final class MarkerManager { guard usableLocked("clear") else { return } entities.removeAll() cellRegistry?.clear() + gridIndex.invalidate() } public func destroy() { @@ -217,6 +243,7 @@ public final class MarkerManager { entities.removeAll() cellRegistry?.clear() cellRegistry = nil + gridIndex.invalidate() } public var isDestroyed: Bool { diff --git a/Sources/MapConductorCore/marker/MarkerState.swift b/Sources/MapConductorCore/marker/MarkerState.swift index 4fe0b82..cde7025 100644 --- a/Sources/MapConductorCore/marker/MarkerState.swift +++ b/Sources/MapConductorCore/marker/MarkerState.swift @@ -23,19 +23,23 @@ public final class MarkerState: ObservableObject, Identifiable, Equatable, Hasha public let id: String public var extra: Any? - @Published public var icon: (any MarkerIconProtocol)? - @Published public var clickable: Bool - @Published public var draggable: Bool - @Published public var zIndex: Int? - @Published public var onClick: OnMarkerEventHandler? - @Published public var onDragStart: OnMarkerEventHandler? - @Published public var onDrag: OnMarkerEventHandler? - @Published public var onDragEnd: OnMarkerEventHandler? - @Published public var onAnimateStart: OnMarkerEventHandler? - @Published public var onAnimateEnd: OnMarkerEventHandler? - @Published public var position: GeoPoint - - @Published private var internalAnimation: MarkerAnimation? + /// Writes to the fields below are announced here rather than discovered by + /// subscribing to each state. See ``StateMutationSignal``. + public let mutations = StateMutationSignal() + + @Published public var icon: (any MarkerIconProtocol)? { didSet { mutations.notifyMutated() } } + @Published public var clickable: Bool { didSet { mutations.notifyMutated() } } + @Published public var draggable: Bool { didSet { mutations.notifyMutated() } } + @Published public var zIndex: Int? { didSet { mutations.notifyMutated() } } + @Published public var onClick: OnMarkerEventHandler? { didSet { mutations.notifyMutated() } } + @Published public var onDragStart: OnMarkerEventHandler? { didSet { mutations.notifyMutated() } } + @Published public var onDrag: OnMarkerEventHandler? { didSet { mutations.notifyMutated() } } + @Published public var onDragEnd: OnMarkerEventHandler? { didSet { mutations.notifyMutated() } } + @Published public var onAnimateStart: OnMarkerEventHandler? { didSet { mutations.notifyMutated() } } + @Published public var onAnimateEnd: OnMarkerEventHandler? { didSet { mutations.notifyMutated() } } + @Published public var position: GeoPoint { didSet { mutations.notifyMutated() } } + + @Published private var internalAnimation: MarkerAnimation? { didSet { mutations.notifyMutated() } } public init( position: GeoPoint, diff --git a/Sources/MapConductorCore/marker/MarkerTileRenderer.swift b/Sources/MapConductorCore/marker/MarkerTileRenderer.swift index 77382b3..f4b41c3 100644 --- a/Sources/MapConductorCore/marker/MarkerTileRenderer.swift +++ b/Sources/MapConductorCore/marker/MarkerTileRenderer.swift @@ -1,5 +1,16 @@ import Foundation import UIKit +/// タイル 1 枚の時間の行き先を出すかどうか。 +/// +/// 既定は環境変数 `MC_MARKER_PHASES`。ただし `xcodebuild test` はシェルの環境を +/// テストプロセスへ渡さないので、計測からは直接立てる。 +/// +/// `MarkerTileRenderer` はジェネリックなので static の格納プロパティを持てず、 +/// ここに置いてある。 +public enum MarkerTilePhaseTrace { + public static var enabled = ProcessInfo.processInfo.environment["MC_MARKER_PHASES"] != nil +} + /// A tile renderer for markers that implements `TileProvider`. /// @@ -13,11 +24,27 @@ public final class MarkerTileRenderer: TileProvider { public let extraIconScale: Double private let debugTileOverlay: Bool private let iconScaleCallback: ((MarkerState, Int) -> Double)? + /// Keep one marker per cell of this many pixels, or 0 to keep them all. + private let declutterPx: Int + private var gpuCached: MetalMarkerRasterizer? + private var gpuResolved = false + private let gpuLock = NSLock() private let cacheLock = NSLock() private let cache: NSCache private var cacheVersion: Int = 0 + /// Largest icon half-extent any tile has needed so far, in points. + /// + /// Seeds the padding used to widen a tile's marker query. Starts from the + /// default icon's own extent rather than a guess: the guess was 32pt, real + /// icons are larger, and every tile therefore paid a second query and a + /// second prepare — 102 ms of a 261 ms tile at z6 with 20k markers. + /// + /// `renderTile` can run concurrently and this is only a hint: a lost update + /// costs one extra pass on one tile, which is the very thing it avoids. + private var observedHalfExtentPx: Double + private let defaultIcon: BitmapIcon public init( @@ -26,14 +53,25 @@ public final class MarkerTileRenderer: TileProvider { extraIconScale: Double = 1.0, cacheSizeBytes: Int = 8 * 1024 * 1024, debugTileOverlay: Bool = false, - iconScaleCallback: ((MarkerState, Int) -> Double)? = nil + iconScaleCallback: ((MarkerState, Int) -> Double)? = nil, + declutterPx: Int = 0 ) { self.markerManager = markerManager self.tileSize = tileSize self.extraIconScale = extraIconScale self.debugTileOverlay = debugTileOverlay self.iconScaleCallback = iconScaleCallback - self.defaultIcon = DefaultMarkerIcon().toBitmapIcon() + self.declutterPx = declutterPx + let icon = DefaultMarkerIcon().toBitmapIcon() + self.defaultIcon = icon + let anchorX = Double(icon.anchor.x) + let anchorY = Double(icon.anchor.y) + let width = Double(icon.size.width) * extraIconScale + let height = Double(icon.size.height) * extraIconScale + self.observedHalfExtentPx = max( + max(abs(width * anchorX), abs(width * (1.0 - anchorX))), + max(abs(height * anchorY), abs(height * (1.0 - anchorY))) + ) let cache = NSCache() cache.totalCostLimit = cacheSizeBytes @@ -101,6 +139,52 @@ public final class MarkerTileRenderer: TileProvider { return MarkerHitTest.pickIndex(touch: screenPoint, rects: rects, tolerance: tolerance).map { states[$0] } } + /// タイル 1 枚の時間の行き先を出す。`MC_MARKER_PHASES` が立っているときだけ。 + /// + /// android-vectortile の `MARKERTILE_PHASES` に対応する。合計だけ見ていると + /// 「問い合わせが重いのか、描くのが重いのか」が分からず、打ち手を選べない。 + /// 実測(iPad Pro 11、ズーム 11、17,937 件)では合計 143ms のうち + /// 問い合わせが 29ms で、残り 115ms が準備と描画だった。 + private static var tracePhases: Bool { MarkerTilePhaseTrace.enabled } + + private static func now() -> UInt64 { DispatchTime.now().uptimeNanoseconds } + private static func since(_ start: UInt64) -> Double { + Double(DispatchTime.now().uptimeNanoseconds - start) / 1_000_000 + } + + /// 「マネージャにはマーカーがあるのに、このタイルの問い合わせは空」が + /// 起きた瞬間に呼ばれる。 + /// + /// この組み合わせは、本当に何も無い場所(皇居の堀の中)でも起きるので + /// 即エラーではない。が、取り込みや更新の途中に重なった空振りだと、 + /// 描いた透明タイルが地図 SDK のセッションキャッシュに残り、霞が関に + /// 四角い穴が空いたまま再要求されない -- 実機で起きた形。受け側 + /// (プロバイダ)はこれを合図に、少し置いてタイルキャッシュを捨てて + /// 引き直させる。正しく空だった場所は引き直しても透明のままなので、 + /// 副作用は再描画 1 回で済む。 + public var onEmptyTileWhilePopulated: ((TileRequest) -> Void)? + + /// 同じタイルの問い合わせだけをやり直し、**今も**空かを返す。 + /// + /// `onEmptyTileWhilePopulated` の受け側が使う。空タイルには 2 種類ある: + /// 本当に何も無い場所(何度聞いても空)と、取り込みや更新の途中に重なった + /// 空振り(少し置いて聞き直すとマーカーが居る)。前者でキャッシュを + /// 捨てると全タイルの引き直しがループするので、後者だけを見分ける。 + public func tileStillEmpty(request: TileRequest) -> Bool { + let z = request.z + let worldTileCount = 1 << z + guard request.y >= 0 && request.y < worldTileCount else { return true } + let normalizedX = normalizeTileX(request.x, worldTileCount: worldTileCount) + let nw = tileToGeoPoint(x: Double(normalizedX), y: Double(request.y), z: Double(z)) + let se = tileToGeoPoint(x: Double(normalizedX) + 1.0, y: Double(request.y) + 1.0, z: Double(z)) + let bounds = GeoRectBounds( + southWest: GeoPoint(latitude: se.latitude, longitude: nw.longitude), + northEast: GeoPoint(latitude: nw.latitude, longitude: se.longitude) + ) + return queryByHalfExtentPx(observedHalfExtentPx, bounds: bounds, tilePx: Double(tileSize)) + .isEmpty + } + public func renderTile(request: TileRequest) -> Data? { let z = request.z let worldTileCount = 1 << z @@ -133,74 +217,293 @@ public final class MarkerTileRenderer: TileProvider { northEast: GeoPoint(latitude: nw.latitude, longitude: se.longitude) ) - // First pass: conservative 32pt padding to catch nearby icons - let assumedHalfExtentPx: Double = 32.0 + // Starts at 32pt but widens to whatever a tile has actually needed. + // Left fixed, the "conservative first pass" never pays off — real icons + // are larger than the guess — and the query and prepare passes simply + // run twice for every tile. + let assumedHalfExtentPx: Double = observedHalfExtentPx + let phaseStart = Self.tracePhases ? Self.now() : 0 var entities = queryByHalfExtentPx(assumedHalfExtentPx, bounds: bounds, tilePx: tilePx) + var queryMs = Self.tracePhases ? Self.since(phaseStart) : 0 + var passes = 1 if entities.isEmpty && !debugTileOverlay { + if let hook = onEmptyTileWhilePopulated, markerManager.getMemoryStats().entityCount > 0 { + hook(request) + } + /* + 空のタイルは nil。透明な PNG に変えるのは `LocalTileServer` の仕事。 + + nil を「タイルが無い」と解釈されると困る、という話は変わっていない + (地図 SDK は 404 を恒久記憶し、隣のタイルのはみ出し分だけが残って + 穴の縁でアイコンが半分に切れる — 後楽園で実際に起きた形)。 + 変えたのは**誰が絵にするか**で、いまはサーバが空を透明タイルとして + 200 で返す。ここで自前の透明タイルを返していたときは、同じく空で + nil を返す geojson / kml / groundimage / vectortile だけが 503 に + なっていた。android-sdk と同じく、意味の変換は一箇所に置く。 + */ return nil } + let prepareStart = Self.tracePhases ? Self.now() : 0 var prepared = prepareMarkers(entities, tileX: tileXDouble, tileY: tileYDouble, zoom: z, tilePx: tilePx) + var prepareMs = Self.tracePhases ? Self.since(prepareStart) : 0 // Second pass: re-query if actual icons are larger than assumed if prepared.maxHalfExtentPx > assumedHalfExtentPx + 1.0 { + passes = 2 + observedHalfExtentPx = prepared.maxHalfExtentPx + let requeryStart = Self.tracePhases ? Self.now() : 0 entities = queryByHalfExtentPx(prepared.maxHalfExtentPx, bounds: bounds, tilePx: tilePx) + if Self.tracePhases { queryMs += Self.since(requeryStart) } + let reprepareStart = Self.tracePhases ? Self.now() : 0 prepared = prepareMarkers(entities, tileX: tileXDouble, tileY: tileYDouble, zoom: z, tilePx: tilePx) + if Self.tracePhases { prepareMs += Self.since(reprepareStart) } } + let drawStart = Self.tracePhases ? Self.now() : 0 let paddingPx = max(Int(ceil(prepared.maxHalfExtentPx + 2.0)), 2) - let offscreenSize = tileSize + paddingPx * 2 - let format = UIGraphicsImageRendererFormat() - format.scale = 1.0 - format.opaque = false - - // Draw all markers into an offscreen canvas (tile + padding) - let offscreenRenderer = UIGraphicsImageRenderer( - size: CGSize(width: offscreenSize, height: offscreenSize), - format: format - ) - let offscreenImage = offscreenRenderer.image { ctx in - if debugTileOverlay { - let o = CGFloat(paddingPx) - let cgCtx = ctx.cgContext - cgCtx.setStrokeColor(UIColor.red.cgColor) - cgCtx.setLineWidth(1.0) - cgCtx.move(to: CGPoint(x: o, y: o)) - cgCtx.addLine(to: CGPoint(x: o + CGFloat(tileSize), y: o)) - cgCtx.strokePath() - cgCtx.move(to: CGPoint(x: o, y: o)) - cgCtx.addLine(to: CGPoint(x: o, y: o + CGFloat(tileSize))) - cgCtx.strokePath() + + // A bitmap context we own, not UIGraphicsImageRenderer. + // + // The renderer records the drawing and replays it when the image is + // materialised, so its cost scales with the number of draw calls rather + // than the size of the canvas: 20k blits into a 612px buffer measured + // 534 ms through it against 58 ms into a plain bitmap context on an + // iPad Pro. The renderer's own profile showed the same shape — the draw + // loop reported 81 ms while the block containing it took 1739 ms. + // + // Owning the buffer also removes the crop pass and the copy the encoder + // used to need: markers are drawn straight into a tile-sized canvas + // that is translated by the padding, so anything overhanging the edge is + // clipped where it used to be composited and then cut away. + let bytesPerRow = tileSize * 4 + let pixels = NSMutableData(length: bytesPerRow * tileSize)! + guard let context = CGContext( + data: pixels.mutableBytes, + width: tileSize, + height: tileSize, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return nil } + + // Core Graphics puts the origin at the bottom left and draws images + // bottom-up; tile coordinates run top-down. Flipping here keeps every + // destination rectangle below in tile coordinates. + context.translateBy(x: 0, y: CGFloat(tileSize)) + context.scaleBy(x: 1, y: -1) + context.translateBy(x: CGFloat(-paddingPx), y: CGFloat(-paddingPx)) + context.interpolationQuality = .none + + if debugTileOverlay { + // 上と左の線。タイルが敷き詰められれば格子として読める。 + let o = CGFloat(paddingPx) + // 目立たせない。この格子は「どのタイルか」を読むための定規で、 + // 主役はマーカーの絵のほう。赤だと視線がそちらに奪われる。 + context.setStrokeColor(UIColor.gray.withAlphaComponent(0.45).cgColor) + context.setLineWidth(1.0) + context.move(to: CGPoint(x: o, y: o)) + context.addLine(to: CGPoint(x: o + CGFloat(tileSize), y: o)) + context.strokePath() + context.move(to: CGPoint(x: o, y: o)) + context.addLine(to: CGPoint(x: o, y: o + CGFloat(tileSize))) + context.strokePath() + + // タイル番号。android-sdk のデバッグ描画と同じ内容で、継ぎ目の + // 不具合報告を「どの 2 枚の間か」まで特定できるようにする。 + // flip 済みの context は UIKit と同じ向きなので、そのまま文字が描ける。 + UIGraphicsPushContext(context) + let text = "x/y/z=\(normalizedX)/\(tileY)/\(z), entries=\(entities.count)" + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.monospacedSystemFont(ofSize: 20, weight: .medium), + .foregroundColor: UIColor.gray.withAlphaComponent(0.8), + ] + let shadow: [NSAttributedString.Key: Any] = [ + .font: UIFont.monospacedSystemFont(ofSize: 20, weight: .medium), + .foregroundColor: UIColor.white.withAlphaComponent(0.8), + ] + let at = CGPoint(x: o + 10, y: o + 8) + (text as NSString).draw(at: CGPoint(x: at.x + 1, y: at.y + 1), withAttributes: shadow) + (text as NSString).draw(at: at, withAttributes: attributes) + UIGraphicsPopContext() + } + + // The cell the tile's own corner falls in. Subtracting it keeps the + // group numbers small without moving the grid: two tiles still agree + // about which markers share a cell, which is the only thing the + // grouping is asked for. android-sdk needs the small numbers — it packs + // the group and an index into one Long — and the three stay identical. + let cellPx = Double(max(declutterPx, 1)) + let baseCellX = floor(tileXDouble * tilePx / cellPx) + let baseCellY = floor(tileYDouble * tilePx / cellPx) + + var placements = [MarkerPlacement?](repeating: nil, count: prepared.markers.count) + var groups = [MarkerPlacement?](repeating: nil, count: prepared.markers.count) + var images = [CGImage?](repeating: nil, count: prepared.markers.count) + var worldKeys = [Double](repeating: 0, count: prepared.markers.count) + for (index, m) in prepared.markers.enumerated() { + guard let bitmap = m.bitmap.cgImage else { continue } + let centerX = m.centerNormX * tilePx + Double(paddingPx) + let centerY = m.centerNormY * tilePx + Double(paddingPx) + let drawW = Double(m.drawW) + let drawH = Double(m.drawH) + // Whole pixels, deliberately. The destination comes out of a + // projection, so it lands on a fraction of a pixel almost every + // time, and drawing to a non-integer rectangle makes the context + // resample every marker. The same change measured 20x on Android + // and 7x in Chromium; rounding moves a pin by at most half a pixel, + // which is not visible at icon scale. + images[index] = bitmap + let anchorX: Double = Double(m.anchor.x) + let anchorY: Double = Double(m.anchor.y) + let left: Double = (centerX - drawW * anchorX).rounded() + let top: Double = (centerY - drawH * anchorY).rounded() + let width: Double = max(1.0, drawW.rounded()) + let height: Double = max(1.0, drawH.rounded()) + placements[index] = MarkerPlacement( + left: Int(left), + top: Int(top), + width: Int(width), + height: Int(height), + icon: ObjectIdentifier(m.bitmap) + ) + // The declutter cell is anchored to the world, not to this tile. + // + // `centerNormX` is the position within this tile, so a cell keyed + // off it moves with the tile: the same ground lands in a different + // cell on the tile next door, the two tiles keep different markers, + // and a marker drawn on one and dropped on the other is **cut at + // the seam.** Adding the tile's own coordinate back gives the world + // tile coordinate the projection produced, which is the same number + // whichever tile is asking. + let worldX = (m.centerNormX + tileXDouble) * tilePx + let worldY = (m.centerNormY + tileYDouble) * tilePx + groups[index] = Self.declutterGroup( + declutterPx: declutterPx, + worldX: worldX, + worldY: worldY, + baseX: baseCellX, + baseY: baseCellY, + placement: placements[index]! + ) + // セルの勝者を決める正準キー。下の間引きループの説明を参照。 + worldKeys[index] = worldY * 1_048_576.0 + worldX + } + + // Drop markers that are completely hidden by a later one. + // + // Zoom out far enough and a whole city collapses onto a few hundred + // pixels: at z6 a dataset of 20k markers resolves to roughly 2k + // distinct positions, and the other 18k are drawn underneath copies of + // themselves. Keeping the last of each group is what would have been + // visible anyway, since drawing is in painter's order. + // + // Only exact agreement counts — same rectangle, same icon — so nothing + // that could peek out from behind another is dropped. Two identical + // icons stacked do differ from one where the icon's own edges are + // partly transparent, by a fraction of a level; that difference is + // overdraw rather than intent. + // With decluttering on the group is a cell rather than a rectangle, and + // the icon is not part of it: the caller has said markers that close + // together are interchangeable, so one of them stands for the rest + // whatever they draw. + // + // 間引きの勝者は「列挙順で最後」では決めない。列挙順は index のセルを + // 歩く順で、それは**タイルごとに違う**。同じ間引きセルを共有する隣の + // タイルが別の順で歩けば別の勝者を選び、境界では 2 本の木が半分ずつ + // 縫い合わさる -- 赤と茶が 1 つの丸に重なって見えたのがこれ。勝者は + // 世界座標のキー(worldY 優先、次に worldX、同座標はアイコンの識別子) + // だけから決める。どちらのタイルが聞いても同じ答えになる。 + // 間引きが無いときは今まで通り「最後」でよい: グループは同一矩形 + + // 同一アイコンなので、どれを描いても絵は変わらない。 + var lastAt: [MarkerPlacement: Int] = [:] + lastAt.reserveCapacity(placements.count) + if declutterPx > 0 { + for (index, group) in groups.enumerated() { + guard let group else { continue } + if let current = lastAt[group] { + let a = worldKeys[index] + let b = worldKeys[current] + let iconA = UInt(bitPattern: placements[index]!.icon.hashValue) + let iconB = UInt(bitPattern: placements[current]!.icon.hashValue) + if a > b || (a == b && iconA > iconB) { + lastAt[group] = index + } + } else { + lastAt[group] = index + } + } + } else { + for (index, group) in groups.enumerated() { + if let group { lastAt[group] = index } } + } - for m in prepared.markers { - let centerX = m.centerNormX * tilePx + Double(paddingPx) - let centerY = m.centerNormY * tilePx + Double(paddingPx) - let drawW = Double(m.drawW) - let drawH = Double(m.drawH) - let anchorX = Double(m.anchor.x) - let anchorY = Double(m.anchor.y) - let destRect = CGRect( - x: centerX - drawW * anchorX, - y: centerY - drawH * anchorY, - width: drawW, - height: drawH - ) - m.bitmap.draw(in: destRect) + // 残すものだけ先に集める。GPU 経路はこれをそのままインスタンスにする。 + var kept: [(placement: MarkerPlacement, bitmap: CGImage)] = [] + kept.reserveCapacity(lastAt.count) + for (index, placement) in placements.enumerated() { + guard let placement, let group = groups[index], let bitmap = images[index] else { continue } + guard lastAt[group] == index else { continue } + kept.append((placement, bitmap)) + } + + // GPU で描けたらそれを使う。描けなければ下の CPU 経路へ落ちる -- + // Metal が無い端末、アトラスに収まらない絵、パイプラインを拒む環境。 + if !debugTileOverlay, + let gpu = gpuRasterizer(), + let png = Self.renderOnGpu(gpu, kept: kept, paddingPx: paddingPx) { + if Self.tracePhases { + print(String(format: "MARKERTILE_PHASES z=%d markers=%d passes=%d query=%.1fms prepare=%.1fms gpuDraw=%.1fms", + z, prepared.markers.count, passes, queryMs, prepareMs, Self.since(drawStart))) + } + cacheLock.lock() + if versionSnapshot == cacheVersion { + cache.setObject(png as NSData, forKey: NSNumber(value: key), cost: png.count) } + cacheLock.unlock() + return png } - // Crop to tile bounds - let finalRenderer = UIGraphicsImageRenderer( - size: CGSize(width: tileSize, height: tileSize), - format: format - ) - let finalImage = finalRenderer.image { _ in - offscreenImage.draw(at: CGPoint(x: -paddingPx, y: -paddingPx)) + for (placement, bitmap) in kept { + + // Drawn upside down and flipped back, because the context's own + // flip above would otherwise turn every icon over. + context.saveGState() + context.translateBy(x: CGFloat(placement.left), + y: CGFloat(placement.top + placement.height)) + context.scaleBy(x: 1, y: -1) + context.draw(bitmap, in: CGRect(x: 0, y: 0, + width: CGFloat(placement.width), + height: CGFloat(placement.height))) + context.restoreGState() } - guard let pngData = finalImage.pngData() else { return nil } + let drawMs = Self.tracePhases ? Self.since(drawStart) : 0 + let encodeStart = Self.tracePhases ? Self.now() : 0 + + // The buffer is already what the encoder wants, so there is no image to + // wrap it in and unwrap again. + let pngData: Data + if let encoded = TilePngEncoder.encode( + rgba: pixels.mutableBytes, width: tileSize, height: tileSize, premultiplied: true + ) { + pngData = encoded + } else if let image = context.makeImage().map({ UIImage(cgImage: $0) }), + let fallback = image.pngData() { + pngData = fallback + } else { + return nil + } + + if Self.tracePhases { + let encodeMs = Self.since(encodeStart) + print(String( + format: "MARKERTILE_PHASES z=%d markers=%d passes=%d query=%.1fms prepare=%.1fms draw=%.1fms encode=%.1fms", + z, prepared.markers.count, passes, queryMs, prepareMs, drawMs, encodeMs)) + } cacheLock.lock() if versionSnapshot == cacheVersion { @@ -213,6 +516,7 @@ public final class MarkerTileRenderer: TileProvider { // MARK: - Private + private struct PreparedMarker { let bitmap: UIImage let centerNormX: Double @@ -227,6 +531,90 @@ public final class MarkerTileRenderer: TileProvider { let maxHalfExtentPx: Double } + /// GPU ラスタライザ。最初に要求されたときだけ作る。 + /// + /// Metal が無ければ以後 nil を返し続ける。毎タイル試して毎回失敗するのは + /// 無駄なので、結果を覚える。 + private func gpuRasterizer() -> MetalMarkerRasterizer? { + gpuLock.lock() + defer { gpuLock.unlock() } + if gpuResolved { return gpuCached } + gpuResolved = true + /* + 既定は GPU。`MAPCONDUCTOR_MARKER_TILE_CPU=1` で切り分け用に CPU へ + 落とせる。 + + 一時期 GPU を既定から外していた。アトラスの UV が CGContext の + 下原点を考慮しておらず 1 行反転ぶん別のアイコンを指し、実機で + 「タイル右端のマーカーが消える」「1 つの丸に 2 色」という継ぎ目の + 割れになっていたため(詳細は MetalMarkerRasterizer.makeAtlas)。 + 根治後、`MetalMarkerRasterizerTests`(単体)と + `DeviceSeamReproBench`(実機の GPU/CPU 突き合わせ)が回帰網。 + */ + if ProcessInfo.processInfo.environment["MAPCONDUCTOR_MARKER_TILE_CPU"] == "1" { + return nil + } + gpuCached = MetalMarkerRasterizer.createOrNull(tileSize: tileSize) + return gpuCached + } + + /// 残ったマーカーをインスタンスにして GPU へ渡す。 + /// + /// 絵は種類が少ない(街路樹サンプルで 101 種)。同じ `CGImage` を指している + /// ものをまとめてアトラスへ詰め、インスタンスごとには矩形と UV だけ渡す。 + private static func renderOnGpu( + _ gpu: MetalMarkerRasterizer, + kept: [(placement: MarkerPlacement, bitmap: CGImage)], + paddingPx: Int + ) -> Data? { + guard !kept.isEmpty else { return nil } + let atlasStart = tracePhases ? now() : 0 + + var uniqueImages: [CGImage] = [] + var slotOf: [ObjectIdentifier: Int] = [:] + var slots: [Int] = [] + slots.reserveCapacity(kept.count) + for entry in kept { + let identity = ObjectIdentifier(entry.bitmap) + if let slot = slotOf[identity] { + slots.append(slot) + } else { + let slot = uniqueImages.count + slotOf[identity] = slot + uniqueImages.append(entry.bitmap) + slots.append(slot) + } + } + + guard let atlas = gpu.makeAtlas(uniqueImages) else { return nil } + + var instances: [MetalMarkerRasterizer.Instance] = [] + instances.reserveCapacity(kept.count) + for (at, entry) in kept.enumerated() { + let uv = atlas.uv[slots[at]] + instances.append(MetalMarkerRasterizer.Instance( + left: Float(entry.placement.left), + top: Float(entry.placement.top), + width: Float(entry.placement.width), + height: Float(entry.placement.height), + u0: Float(uv.minX), v0: Float(uv.minY), + u1: Float(uv.maxX), v1: Float(uv.maxY) + )) + } + // アトラスと描画を分けて出す。合わせて "gpuDraw" だが、中身は + // マーカーの数に比例する部分(残ったぶんを並べる)と、しない部分 + // (テクスチャを作る・読み戻す・PNG にする)に割れている。どちらを + // 削るかで打ち手が違う。 + let atlasMs = tracePhases ? since(atlasStart) : 0 + let drawStart = tracePhases ? now() : 0 + let png = gpu.renderPng(instances: instances, atlas: atlas.texture, paddingPx: paddingPx) + if tracePhases { + print(String(format: "MARKERTILE_GPU kept=%d icons=%d atlas=%.1fms drawEncode=%.1fms", + kept.count, uniqueImages.count, atlasMs, since(drawStart))) + } + return png + } + private func prepareMarkers( _ entities: [MarkerEntity], tileX: Double, @@ -277,7 +665,16 @@ public final class MarkerTileRenderer: TileProvider { tilePx: Double ) -> [MarkerEntity] { guard let span = bounds.toSpan() else { return [] } - let padNorm = max(halfExtentPx / tilePx, 0.0) + // A whole declutter cell beyond the icon overhang. + // + // The overhang alone is enough to draw the tile, but not enough to + // decide it: a declutter cell straddling the edge would have some of + // its markers inside the query and some outside, and the tile next door + // would see a different part of the same cell. Both would keep a marker + // and the two would not be the same one, which is an icon cut at the + // seam. Growing by the cell as well means every cell that reaches this + // tile is here in full, so both tiles see all of it and agree. + let padNorm = max((halfExtentPx + Double(declutterPx)) / tilePx, 0.0) let latPad = span.latitude * padNorm let lonPad = span.longitude * padNorm let expanded = bounds.expandedByDegrees(latPad: latPad, lonPad: lonPad) @@ -290,7 +687,23 @@ public final class MarkerTileRenderer: TileProvider { // // longdo のようにタイル専用の manager を別に持つプロバイダでは全 entity が // tiling = true なので、この絞り込みは何もしない。 - return markerManager.findMarkersInBounds(expanded).filter { $0.tiling } + // declutter が効いているときは index の段階で間引く。 + // + // 呼び出し側が「これより近いマーカーは入れ替え可能」と言っているので、 + // index は自分のセルから答えてよい。zoom 9 なら東京の街路樹 144,183 本 + // ではなく、それを含む 5,000 セルほどを読むだけで済む。 + // + // ここを飛ばして後段の辞書で間引いていたときは、捨てるぶんの配置と + // 画像まで先に作っていたので、declutter を入れるほど遅くなっていた + // (20,000 マーカーで 82.8ms -> 178.5ms)。index が粗すぎて分離を + // 保証できない場合は全件クエリに落ちるので、後段の間引きは残してある。 + let separationDegrees = declutterPx > 0 + ? max(span.latitude, span.longitude) * Double(declutterPx) / tilePx + : 0.0 + let found = separationDegrees > 0.0 + ? markerManager.findMarkersInBounds(expanded, minSeparationDegrees: separationDegrees) + : markerManager.findMarkersInBounds(expanded) + return found.filter { $0.tiling } } private func tileToGeoPoint(x: Double, y: Double, z: Double) -> GeoPoint { @@ -349,3 +762,54 @@ public final class MarkerTileRenderer: TileProvider { private let maxMercatorLat: Double = 85.05112878 } + +/// Where a marker is drawn, and which icon it draws. +/// +/// Markers that agree on all of it sit exactly on top of one another, so all +/// but the last are invisible. +extension MarkerTileRenderer { + /// What counts as "the same place" for the pass that drops hidden markers. + /// What a marker is grouped by when deciding which ones to drop. + /// + /// Without decluttering that is the placement itself — same rectangle, same + /// icon — so only markers drawn exactly on top of each other collapse. + /// + /// With decluttering it is a cell of `declutterPx`, taken in **world** + /// pixels at this zoom rather than tile pixels, and the icon is not part of + /// it: the caller has said markers that close together are interchangeable, + /// so one of them stands for the rest whatever they draw. The world anchor + /// is what makes two tiles agree about which one that is. + fileprivate static func declutterGroup( + declutterPx: Int, + worldX: Double, + worldY: Double, + baseX: Double, + baseY: Double, + placement: MarkerPlacement + ) -> MarkerPlacement { + guard declutterPx > 0 else { return placement } + let cell = Double(declutterPx) + return MarkerPlacement( + left: Int(floor(worldX / cell) - baseX), + top: Int(floor(worldY / cell) - baseY), + width: 0, + height: 0, + icon: MarkerPlacement.anyIcon + ) + } +} + +private struct MarkerPlacement: Hashable { + let left: Int + let top: Int + let width: Int + let height: Int + let icon: ObjectIdentifier + + /// Stands in for "any icon" when the group is a cell rather than a + /// rectangle. A cell holds whatever it holds. + static let anyIcon = ObjectIdentifier(AnyIconMarker.self) + + private final class AnyIconMarker {} +} + diff --git a/Sources/MapConductorCore/marker/MarkerTilingOptions.swift b/Sources/MapConductorCore/marker/MarkerTilingOptions.swift index 1b1b01b..a996240 100644 --- a/Sources/MapConductorCore/marker/MarkerTilingOptions.swift +++ b/Sources/MapConductorCore/marker/MarkerTilingOptions.swift @@ -14,6 +14,19 @@ public struct MarkerTilingOptions { public let cacheSize: Int /// Extra scale multiplier applied per marker per zoom level during tile rendering. public let iconScaleCallback: ((MarkerState, Int) -> Double)? + /// Keep one marker per cell of this many pixels, or 0 to keep them all. + /// + /// Markers a few pixels apart overlap almost completely, so thinning them is + /// a judgement about the map rather than a free optimisation — hence opt-in. + /// Tokyo's street trees are planted metres apart along a road: below street + /// level most of them sit on top of one another, and one per icon width + /// halves the tile bytes while showing the same map. + /// + /// Zero still drops markers that agree exactly — same rectangle, same icon — + /// which cannot change the tile. + /// + /// android-sdk's and the web SDK's MarkerTilingOptions carry the same field. + public let declutterPx: Int public static let Disabled = MarkerTilingOptions(enabled: false) public static let Default = MarkerTilingOptions() @@ -23,13 +36,15 @@ public struct MarkerTilingOptions { debugTileOverlay: Bool = false, minMarkerCount: Int = 2000, cacheSize: Int = 8 * 1024 * 1024, - iconScaleCallback: ((MarkerState, Int) -> Double)? = nil + iconScaleCallback: ((MarkerState, Int) -> Double)? = nil, + declutterPx: Int = 0 ) { self.enabled = enabled self.debugTileOverlay = debugTileOverlay self.minMarkerCount = minMarkerCount self.cacheSize = cacheSize self.iconScaleCallback = iconScaleCallback + self.declutterPx = declutterPx } /// タイル方式で描くか。**この判定を各所で書き直さないこと。** diff --git a/Sources/MapConductorCore/marker/MetalMarkerRasterizer.swift b/Sources/MapConductorCore/marker/MetalMarkerRasterizer.swift new file mode 100644 index 0000000..1262383 --- /dev/null +++ b/Sources/MapConductorCore/marker/MetalMarkerRasterizer.swift @@ -0,0 +1,267 @@ +import CoreGraphics +import Metal +import UIKit + +/// マーカータイルを GPU で描く。 +/// +/// ios-vectortile の `MetalTileRasterizer` と同じ骨格 -- オフスクリーンへ描いて +/// 読み戻し、PNG 化は core の Rust エンコーダに渡す。違うのは中身で、あちらは +/// 単色の三角形、こちらはテクスチャ付きのクアッドをインスタンスで並べる。 +/// +/// ## なぜ効くか +/// +/// CPU 側は `CGContext.draw(image:in:)` をマーカーの数だけ呼ぶ。1 回ごとに +/// 状態を触り、クリップを評価し、ブレンドする。GPU 側は描画命令 1 つで、 +/// 位置と大きさだけがインスタンスごとに変わる。実測(iPad Pro 11、768px の +/// タイルへ 17,937 個)で **351.8ms 対 3.8ms** -- 読み戻しを含めて 93 倍。 +/// +/// ## 前提 +/// +/// アイコンは種類が少なく(街路樹サンプルで 101 種)、マーカーの数だけある +/// わけではない。だから 1 枚のアトラスに詰めて、インスタンスごとには UV の +/// 矩形だけ渡せば済む。 +/// +/// Metal が使えないときは `nil` を返す。呼び出し側は CPU 経路へ落ちる。 +final class MetalMarkerRasterizer { + + /// インスタンス 1 件。シェーダの `Instance` と同じ並び。 + struct Instance { + /// 描画先。タイル座標(padding を含んだまま)。 + var left: Float + var top: Float + var width: Float + var height: Float + /// アトラス上の位置。0..1。 + var u0: Float + var v0: Float + var u1: Float + var v1: Float + } + + private let device: MTLDevice + private let commandQueue: MTLCommandQueue + private let pipeline: MTLRenderPipelineState + private let sampler: MTLSamplerState + private let queue = DispatchQueue(label: "com.mapconductor.marker.metal") + + private let tileSize: Int + private var target: MTLTexture? + + private static let shaderSource = """ + #include + using namespace metal; + + struct Instance { + float4 rect; // left, top, width, height(タイル座標) + float4 uv; // u0, v0, u1, v1 + }; + + struct VertexOut { + float4 position [[position]]; + float2 uv; + }; + + vertex VertexOut marker_vertex(uint vid [[vertex_id]], + uint iid [[instance_id]], + constant Instance *instances [[buffer(0)]], + constant float2 &extent [[buffer(1)]], + constant float &padding [[buffer(2)]]) { + // クアッドは頂点 id から組み立てる。頂点バッファは持たない。 + const float2 corners[6] = { float2(0,0), float2(1,0), float2(0,1), + float2(1,0), float2(1,1), float2(0,1) }; + float2 corner = corners[vid]; + Instance inst = instances[iid]; + + // CPU 経路は canvas を -padding だけずらして描く。同じ位置に出す。 + float2 px = inst.rect.xy + corner * inst.rect.zw - padding; + float2 unit = px / extent; + + VertexOut out; + out.position = float4(unit.x * 2.0 - 1.0, 1.0 - unit.y * 2.0, 0.0, 1.0); + out.uv = mix(inst.uv.xy, inst.uv.zw, corner); + return out; + } + + fragment float4 marker_fragment(VertexOut in [[stage_in]], + texture2d atlas [[texture(0)]], + sampler smp [[sampler(0)]]) { + return atlas.sample(smp, in.uv); + } + """ + + /// Metal が使えなければ nil。シミュレータの一部や、パイプラインを拒む端末。 + /// 呼び出し側は失敗ではなく CPU 経路として扱う。 + static func createOrNull(tileSize: Int) -> MetalMarkerRasterizer? { + guard let device = MTLCreateSystemDefaultDevice(), + let commandQueue = device.makeCommandQueue() else { return nil } + do { + return try MetalMarkerRasterizer(device: device, commandQueue: commandQueue, tileSize: tileSize) + } catch { + return nil + } + } + + private init(device: MTLDevice, commandQueue: MTLCommandQueue, tileSize: Int) throws { + self.device = device + self.commandQueue = commandQueue + self.tileSize = tileSize + + let library = try device.makeLibrary(source: Self.shaderSource, options: nil) + let descriptor = MTLRenderPipelineDescriptor() + descriptor.vertexFunction = library.makeFunction(name: "marker_vertex") + descriptor.fragmentFunction = library.makeFunction(name: "marker_fragment") + + let attachment = descriptor.colorAttachments[0]! + attachment.pixelFormat = .rgba8Unorm + attachment.isBlendingEnabled = true + // アトラスは乗算済みアルファで積むので、source は one。 + attachment.rgbBlendOperation = .add + attachment.sourceRGBBlendFactor = .one + attachment.destinationRGBBlendFactor = .oneMinusSourceAlpha + attachment.alphaBlendOperation = .add + attachment.sourceAlphaBlendFactor = .one + attachment.destinationAlphaBlendFactor = .oneMinusSourceAlpha + pipeline = try device.makeRenderPipelineState(descriptor: descriptor) + + let samplerDescriptor = MTLSamplerDescriptor() + // CPU 経路は interpolationQuality = .none。同じ絵にするため最近傍。 + samplerDescriptor.minFilter = .nearest + samplerDescriptor.magFilter = .nearest + guard let sampler = device.makeSamplerState(descriptor: samplerDescriptor) else { + throw MetalMarkerError.unavailable + } + self.sampler = sampler + } + + private enum MetalMarkerError: Error { case unavailable } + + /// 別々の CGImage をひとつのアトラスへ詰め、UV を返す。 + /// + /// アイコンは種類が少ないので、正方に近い格子に等間隔で並べるだけで足りる。 + /// 詰め方を凝っても、ここは 1 タイルに数十枚しか入らない。 + func makeAtlas(_ images: [CGImage]) -> (texture: MTLTexture, uv: [CGRect])? { + guard !images.isEmpty else { return nil } + let cellWidth = images.map(\.width).max() ?? 1 + let cellHeight = images.map(\.height).max() ?? 1 + let columns = Int(ceil(sqrt(Double(images.count)))) + let rows = Int(ceil(Double(images.count) / Double(columns))) + let width = columns * cellWidth + let height = rows * cellHeight + + let bytesPerRow = width * 4 + var pixels = [UInt8](repeating: 0, count: bytesPerRow * height) + var uv: [CGRect] = [] + pixels.withUnsafeMutableBytes { raw in + guard let context = CGContext( + data: raw.baseAddress, width: width, height: height, + bitsPerComponent: 8, bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return } + for (at, image) in images.enumerated() { + let column = at % columns + let row = at / columns + let rect = CGRect(x: column * cellWidth, y: row * cellHeight, + width: image.width, height: image.height) + context.draw(image, in: rect) + /* + v は**下から**測る。 + + `CGContext(data:)` の原点は左下で、「行 r」に置いたつもりの + アイコンはメモリ上では行 rows-1-r に居る。テクスチャはメモリを + そのまま積むので、上基準の v を渡すと**1 行反転ぶん別のアイコン**を + サンプルする -- 単体テストでは 3x3 の行 0 と行 2 がそっくり + 入れ替わり、反転先が最終行の空セルに落ちたアイコンは絵ごと + 消えた。実機では「タイル右端のマーカーが消える」「1 つの丸に + 2 色」という継ぎ目の割れとして見えていた。アイコン自体は + CG がイメージも同じ向きで描くため二重反転で正立しており、 + ずれるのは行の割り当てだけ。 + + 半テクセルの内側詰めは最近傍サンプリングが隣のセルの端を + 拾わないための保険。 + */ + let insetX = 0.5 / Double(width) + let insetY = 0.5 / Double(height) + let flippedY = Double(height) - rect.maxY + uv.append(CGRect( + x: rect.minX / Double(width) + insetX, + y: flippedY / Double(height) + insetY, + width: rect.width / Double(width) - 2 * insetX, + height: rect.height / Double(height) - 2 * insetY + )) + } + } + guard uv.count == images.count else { return nil } + + let descriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: .rgba8Unorm, width: width, height: height, mipmapped: false) + descriptor.usage = .shaderRead + guard let texture = device.makeTexture(descriptor: descriptor) else { return nil } + texture.replace(region: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0, + withBytes: pixels, bytesPerRow: bytesPerRow) + return (texture, uv) + } + + /// 描いて PNG にする。描けなければ nil で、呼び出し側は CPU へ落ちる。 + func renderPng(instances: [Instance], atlas: MTLTexture, paddingPx: Int) -> Data? { + guard !instances.isEmpty else { return nil } + return queue.sync { draw(instances: instances, atlas: atlas, paddingPx: paddingPx) } + } + + private func draw(instances: [Instance], atlas: MTLTexture, paddingPx: Int) -> Data? { + let target: MTLTexture + if let existing = self.target { + target = existing + } else { + let descriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: .rgba8Unorm, width: tileSize, height: tileSize, mipmapped: false) + descriptor.usage = [.renderTarget, .shaderRead] + guard let fresh = device.makeTexture(descriptor: descriptor) else { return nil } + self.target = fresh + target = fresh + } + + guard let instanceBuffer = device.makeBuffer( + bytes: instances, + length: MemoryLayout.stride * instances.count, + options: .storageModeShared + ) else { return nil } + + let pass = MTLRenderPassDescriptor() + pass.colorAttachments[0].texture = target + pass.colorAttachments[0].loadAction = .clear + pass.colorAttachments[0].storeAction = .store + pass.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0) + + guard let buffer = commandQueue.makeCommandBuffer(), + let encoder = buffer.makeRenderCommandEncoder(descriptor: pass) else { return nil } + + var extent = SIMD2(Float(tileSize), Float(tileSize)) + var padding = Float(paddingPx) + encoder.setRenderPipelineState(pipeline) + encoder.setVertexBuffer(instanceBuffer, offset: 0, index: 0) + encoder.setVertexBytes(&extent, length: MemoryLayout>.size, index: 1) + encoder.setVertexBytes(&padding, length: MemoryLayout.size, index: 2) + encoder.setFragmentTexture(atlas, index: 0) + encoder.setFragmentSamplerState(sampler, index: 0) + encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, + instanceCount: instances.count) + encoder.endEncoding() + buffer.commit() + buffer.waitUntilCompleted() + guard buffer.error == nil else { return nil } + + let bytesPerRow = tileSize * 4 + var readback = [UInt8](repeating: 0, count: bytesPerRow * tileSize) + readback.withUnsafeMutableBytes { raw in + target.getBytes(raw.baseAddress!, bytesPerRow: bytesPerRow, + from: MTLRegionMake2D(0, 0, tileSize, tileSize), mipmapLevel: 0) + } + return readback.withUnsafeMutableBytes { raw -> Data? in + guard let base = raw.baseAddress else { return nil } + // ブレンドは乗算済みアルファのまま積むので、CPU 経路と同じ扱い。 + return TilePngEncoder.encode(rgba: base, width: tileSize, height: tileSize, + premultiplied: true) + } + } +} diff --git a/Sources/MapConductorCore/overlay/OverlayCollectableState.swift b/Sources/MapConductorCore/overlay/OverlayCollectableState.swift index f8ff897..7c1e367 100644 --- a/Sources/MapConductorCore/overlay/OverlayCollectableState.swift +++ b/Sources/MapConductorCore/overlay/OverlayCollectableState.swift @@ -8,49 +8,26 @@ import Combine /// type (`AnyObject`) so the collector can hold it by identity, diff instances /// with `!==`, and subscribe to it weakly. /// -/// `overlayChangePublisher()` emits whenever a rendered property changes. It is -/// backed by each state's `asFlow()` — the exact publisher the per-provider -/// controllers used before the collector existed — so routing an overlay through -/// the collector reproduces the old in-place-update behavior (e.g. dragging a -/// polygon vertex re-renders the polygon). The collector forwards each emission -/// to the bound controller's `update(state:)`, which dedupes by `fingerPrint()`. +/// ``StateMutationSignal`` is where a state reports a write to a rendered +/// property, and the collector forwards it to the bound controller's +/// `update(state:)`, which dedupes by `fingerPrint()`. The collector used to +/// learn the same thing by subscribing to each state's `asFlow()`; that cost +/// two seconds to sync 50,000 markers on the main actor, so the states report +/// instead. `asFlow()` itself stays — several provider controllers subscribe to +/// it directly, outside the collector. public protocol OverlayCollectableState: AnyObject { var id: String { get } - func overlayChangePublisher() -> AnyPublisher + var mutations: StateMutationSignal { get } } -extension MarkerState: OverlayCollectableState { - public func overlayChangePublisher() -> AnyPublisher { - asFlow().map { _ in () }.eraseToAnyPublisher() - } -} +extension MarkerState: OverlayCollectableState {} -extension CircleState: OverlayCollectableState { - public func overlayChangePublisher() -> AnyPublisher { - asFlow().map { _ in () }.eraseToAnyPublisher() - } -} +extension CircleState: OverlayCollectableState {} -extension PolylineState: OverlayCollectableState { - public func overlayChangePublisher() -> AnyPublisher { - asFlow().map { _ in () }.eraseToAnyPublisher() - } -} +extension PolylineState: OverlayCollectableState {} -extension PolygonState: OverlayCollectableState { - public func overlayChangePublisher() -> AnyPublisher { - asFlow().map { _ in () }.eraseToAnyPublisher() - } -} +extension PolygonState: OverlayCollectableState {} -extension GroundImageState: OverlayCollectableState { - public func overlayChangePublisher() -> AnyPublisher { - asFlow().map { _ in () }.eraseToAnyPublisher() - } -} +extension GroundImageState: OverlayCollectableState {} -extension RasterLayerState: OverlayCollectableState { - public func overlayChangePublisher() -> AnyPublisher { - asFlow().map { _ in () }.eraseToAnyPublisher() - } -} +extension RasterLayerState: OverlayCollectableState {} diff --git a/Sources/MapConductorCore/overlay/OverlayCollector.swift b/Sources/MapConductorCore/overlay/OverlayCollector.swift index fab83bb..a1a4cd2 100644 --- a/Sources/MapConductorCore/overlay/OverlayCollector.swift +++ b/Sources/MapConductorCore/overlay/OverlayCollector.swift @@ -30,7 +30,10 @@ import Foundation public final class OverlayCollector { private var statesById: [String: S] = [:] private var order: [String] = [] - private var subscriptions: [String: AnyCancellable] = [:] + /// Who is currently reporting mutations. A state reports to at most one + /// collector, so leaving a stale listener on a dropped state would send its + /// writes to a collector that no longer holds it. + private var listening: [String: S] = [:] private var latest: [S] = [] private var membershipHandler: (([S]) -> Void)? @@ -88,10 +91,9 @@ public final class OverlayCollector { var next: [String: S] = [:] for state in states { if let existing = statesById[state.id], existing !== state { - // Same id, new instance → drop the stale subscription and treat - // it as a membership change so the renderer re-adds it. - subscriptions[state.id]?.cancel() - subscriptions.removeValue(forKey: state.id) + // Same id, new instance → stop listening to the old one and + // treat it as a membership change so the renderer re-adds it. + existing.mutations.listen(nil) membershipChanged = true } next[state.id] = state @@ -101,21 +103,22 @@ public final class OverlayCollector { latest = states for id in oldIds.subtracting(newIds) { - subscriptions[id]?.cancel() - subscriptions.removeValue(forKey: id) + listening.removeValue(forKey: id)?.mutations.listen(nil) } if membershipChanged { scheduleMembership() } - for state in states where subscriptions[state.id] == nil { - subscriptions[state.id] = state.overlayChangePublisher() - .receive(on: DispatchQueue.main) - .sink { [weak self, weak state] _ in - guard let self, let state, self.statesById[state.id] != nil else { return } - self.scheduleUpdate(state) - } + for state in listening.values where next[state.id] !== state { + state.mutations.listen(nil) + } + listening = next + for state in states { + state.mutations.listen { [weak self, weak state] in + guard let self, let state, self.statesById[state.id] === state else { return } + self.scheduleUpdate(state) + } } } @@ -196,8 +199,8 @@ public final class OverlayCollector { } public func clear() { - subscriptions.values.forEach { $0.cancel() } - subscriptions.removeAll() + listening.values.forEach { $0.mutations.listen(nil) } + listening.removeAll() statesById.removeAll() order.removeAll() latest.removeAll() diff --git a/Sources/MapConductorCore/overlay/StateMutationSignal.swift b/Sources/MapConductorCore/overlay/StateMutationSignal.swift new file mode 100644 index 0000000..207ed3d --- /dev/null +++ b/Sources/MapConductorCore/overlay/StateMutationSignal.swift @@ -0,0 +1,37 @@ +import Foundation + +/// Where an overlay state reports that one of its own fields was written. +/// +/// The collector used to learn this by subscribing to each state's Combine +/// publisher. That is one `map`, one `eraseToAnyPublisher`, one `receive(on:)` +/// and one `sink` per state, plus the `AnyCancellable` holding them, and it +/// does not stay linear: syncing markers into a collector measured 47 ms at +/// 1,000, 236 ms at 10,000 and 2,175 ms at 50,000 on a simulator, on the main +/// actor. Tokyo's street trees are 144,183. +/// +/// A closure per state costs an allocation and nothing else. `objectWillChange` +/// instead of the fingerprint chain was measured too and came out worse (2,898 +/// ms at 50,000): the cost is the subscription machinery, not the chain fed +/// into it. +/// +/// android-sdk carries the same type for the same reason, though it arrived +/// there from the opposite direction — one observer reading every state, rather +/// than one subscription per state. +public final class StateMutationSignal { + private var listener: (() -> Void)? + + public init() {} + + /// Sets who hears about writes, or clears it with nil. + /// + /// A state belongs to at most one collector, so there is one listener + /// rather than a list. + public func listen(_ listener: (() -> Void)?) { + self.listener = listener + } + + /// Reports a write. + public func notifyMutated() { + listener?() + } +} diff --git a/Sources/MapConductorCore/polygon/PolygonState.swift b/Sources/MapConductorCore/polygon/PolygonState.swift index 5bc0422..c156d7f 100644 --- a/Sources/MapConductorCore/polygon/PolygonState.swift +++ b/Sources/MapConductorCore/polygon/PolygonState.swift @@ -30,15 +30,19 @@ public typealias OnPolygonEventHandler = (PolygonEvent) -> Void public final class PolygonState: ObservableObject, Identifiable, Equatable, Hashable { public let id: String - @Published public var strokeColor: UIColor - @Published public var strokeWidth: Double - @Published public var fillColor: UIColor - @Published public var geodesic: Bool - @Published public var zIndex: Int - @Published public var points: [GeoPointProtocol] - @Published public var holes: [[GeoPointProtocol]] - @Published public var extra: Any? - @Published public var onClick: OnPolygonEventHandler? + /// Writes to the fields below are announced here rather than discovered by + /// subscribing to each state. See ``StateMutationSignal``. + public let mutations = StateMutationSignal() + + @Published public var strokeColor: UIColor { didSet { mutations.notifyMutated() } } + @Published public var strokeWidth: Double { didSet { mutations.notifyMutated() } } + @Published public var fillColor: UIColor { didSet { mutations.notifyMutated() } } + @Published public var geodesic: Bool { didSet { mutations.notifyMutated() } } + @Published public var zIndex: Int { didSet { mutations.notifyMutated() } } + @Published public var points: [GeoPointProtocol] { didSet { mutations.notifyMutated() } } + @Published public var holes: [[GeoPointProtocol]] { didSet { mutations.notifyMutated() } } + @Published public var extra: Any? { didSet { mutations.notifyMutated() } } + @Published public var onClick: OnPolygonEventHandler? { didSet { mutations.notifyMutated() } } /// android-sdk の `Polygon(state)` コンポーザブルは `LaunchedEffect(state)` の中で /// `unionHolesInPlace()` を呼ぶため、穴のユニオンは「1 つの state インスタンスにつき 1 回」 diff --git a/Sources/MapConductorCore/polyline/PolylineState.swift b/Sources/MapConductorCore/polyline/PolylineState.swift index 2ca0c5b..8b94cfc 100644 --- a/Sources/MapConductorCore/polyline/PolylineState.swift +++ b/Sources/MapConductorCore/polyline/PolylineState.swift @@ -30,13 +30,17 @@ public typealias OnPolylineEventHandler = (PolylineEvent) -> Void public final class PolylineState: ObservableObject, Identifiable, Equatable, Hashable { public let id: String - @Published public var strokeColor: UIColor - @Published public var strokeWidth: Double - @Published public var geodesic: Bool - @Published public var points: [GeoPointProtocol] - @Published public var zIndex: Int - @Published public var extra: Any? - @Published public var onClick: OnPolylineEventHandler? + /// Writes to the fields below are announced here rather than discovered by + /// subscribing to each state. See ``StateMutationSignal``. + public let mutations = StateMutationSignal() + + @Published public var strokeColor: UIColor { didSet { mutations.notifyMutated() } } + @Published public var strokeWidth: Double { didSet { mutations.notifyMutated() } } + @Published public var geodesic: Bool { didSet { mutations.notifyMutated() } } + @Published public var points: [GeoPointProtocol] { didSet { mutations.notifyMutated() } } + @Published public var zIndex: Int { didSet { mutations.notifyMutated() } } + @Published public var extra: Any? { didSet { mutations.notifyMutated() } } + @Published public var onClick: OnPolylineEventHandler? { didSet { mutations.notifyMutated() } } public init( points: [GeoPointProtocol], diff --git a/Sources/MapConductorCore/raster/RasterLayerController.swift b/Sources/MapConductorCore/raster/RasterLayerController.swift index f894432..c5adda8 100644 --- a/Sources/MapConductorCore/raster/RasterLayerController.swift +++ b/Sources/MapConductorCore/raster/RasterLayerController.swift @@ -1,4 +1,5 @@ -open class RasterLayerController: OverlayControllerProtocol, SlottedOverlayController +open class RasterLayerController: OverlayControllerProtocol, SlottedOverlayController, + RasterLayerMounting where Renderer.ActualLayer == ActualLayer { public typealias StateType = RasterLayerState public typealias EntityType = RasterLayerEntity diff --git a/Sources/MapConductorCore/raster/RasterLayerState.swift b/Sources/MapConductorCore/raster/RasterLayerState.swift index ecca139..b61a6fe 100644 --- a/Sources/MapConductorCore/raster/RasterLayerState.swift +++ b/Sources/MapConductorCore/raster/RasterLayerState.swift @@ -32,13 +32,17 @@ public final class RasterLayerState: ObservableObject, Identifiable, Equatable, public let id: String - @Published public var source: RasterLayerSource - @Published public var opacity: Double - @Published public var visible: Bool - @Published public var zIndex: Int - @Published public var debug: Bool - @Published public var userAgent: String? - @Published public var extraHeaders: [String: String]? + /// Writes to the fields below are announced here rather than discovered by + /// subscribing to each state. See ``StateMutationSignal``. + public let mutations = StateMutationSignal() + + @Published public var source: RasterLayerSource { didSet { mutations.notifyMutated() } } + @Published public var opacity: Double { didSet { mutations.notifyMutated() } } + @Published public var visible: Bool { didSet { mutations.notifyMutated() } } + @Published public var zIndex: Int { didSet { mutations.notifyMutated() } } + @Published public var debug: Bool { didSet { mutations.notifyMutated() } } + @Published public var userAgent: String? { didSet { mutations.notifyMutated() } } + @Published public var extraHeaders: [String: String]? { didSet { mutations.notifyMutated() } } public init( source: RasterLayerSource, diff --git a/Sources/MapConductorCore/raster/RasterTilePreference.swift b/Sources/MapConductorCore/raster/RasterTilePreference.swift new file mode 100644 index 0000000..9b21f74 --- /dev/null +++ b/Sources/MapConductorCore/raster/RasterTilePreference.swift @@ -0,0 +1,55 @@ +import Foundation + +/// どの大きさのタイルなら、この地図 SDK がうまく扱えるか。 +/// +/// タイルの一辺は本来アプリの好みで決めてよく、既定は 512。1 枚あたりの固定費 +/// (ローカルタイルサーバへの往復、PNG の符号化・復号、レンダラの立ち上げ)が +/// 枚数に比例するので、同じ画面を覆うなら枚数は少ないほうが安い。 +/// +/// ところが SDK 側に都合があることがあり、その都合はタイルを供給する側からは +/// 見えない。実例が ArcGIS の 3D SceneView で、**タイルは 256px** という前提で +/// レベルを選ぶ。512px のタイルを渡すと 1 段深いレベルを 4 倍の枚数で取りに行く +/// (Android 実機・統一ズーム 12 で、2D は z=11、3D は z=12)。絵は正しいので +/// 気づきにくく、遅い端末ほど効く。 +/// +/// android-sdk の `RasterTilePreference.kt`、react-sdk の +/// `RasterTilePreference.ts` と同じもの。 +public protocol RasterTilePreference { + /// この SDK に渡してほしいタイルの一辺(ポイント)。 + var preferredTileSize: Int { get } + + /// この SDK がラスタータイルを **物理ピクセル** でレイアウトするなら true。 + /// + /// 地図 SDK には 2 つの流儀がある。宣言された一辺をポイントとして敷くものと、 + /// 物理ピクセルとして敷くもの。後者に 2x の画面で「512」と宣言すると + /// 256 pt しか覆わず、1 段深いズームを要求してスタイルをそこで読む — + /// ラベルは半分、道路は倍の密度になる(web の HERE で実測)。供給側は + /// この流儀を知りようがないので、プロバイダが言う。 + /// + /// true のとき、供給側は ``preferredTileSize`` × scale を宣言し、描画は + /// 同じピクセル数で行い、スタイルには ``preferredTileSize`` pt として + /// 読ませる。既定は false。android-sdk / react-sdk の同名フィールドに対応。 + var layoutInDevicePixels: Bool { get } +} + +extension RasterTilePreference { + public var layoutInDevicePixels: Bool { false } +} + +/// ``RasterTilePreference`` の登録キー。 +/// +/// 宣言しないプロバイダは「好みは無い」。その場合は供給側の既定が使われる。 +public enum RasterTilePreferenceKey: MapServiceKey { + public typealias Value = RasterTilePreference +} + +/// 値だけを持つ既定の実装。プロバイダが 1 行で登録できるように。 +public struct FixedRasterTilePreference: RasterTilePreference { + public let preferredTileSize: Int + public let layoutInDevicePixels: Bool + + public init(preferredTileSize: Int, layoutInDevicePixels: Bool = false) { + self.preferredTileSize = preferredTileSize + self.layoutInDevicePixels = layoutInDevicePixels + } +} diff --git a/Sources/MapConductorCore/tileserver/TilePngEncoder.swift b/Sources/MapConductorCore/tileserver/TilePngEncoder.swift new file mode 100644 index 0000000..f627599 --- /dev/null +++ b/Sources/MapConductorCore/tileserver/TilePngEncoder.swift @@ -0,0 +1,113 @@ +import CoreGraphics +import CTilePng +import Foundation +import UIKit + +/** + PNG encoding in Rust, for tiles the SDK rasterises itself. + + Lives in the core so an app carries one copy of the encoder rather than one per + module that draws tiles. The tiles are ephemeral payloads for a local tile + server, so it trades ratio for speed and produces files a few times larger — + which never reach a network, though they do take proportionally more room in a + tile cache. + + How much it wins depends entirely on the platform encoder it replaces. On an + iPad Pro it is 2.3-2.6x faster than `pngData()`; on a Pixel 5a the same code is + 6-20x faster than `Bitmap.compress`, because Apple's encoder is good and + Android's is not. + + Every failure path returns nil rather than throwing, so every caller is + expected to have a platform fallback: a failed optimisation should cost a slow + tile, never a broken one. + */ +public enum TilePngEncoder { + + private static let ok: Int32 = 0 + + /// Scratch pixel buffer, one per calling thread. + /// + /// Tile rendering runs concurrently, and a tile is several megabytes of + /// RGBA. Allocating that per call cost as much as the encoding itself: + /// 7.9 ms per tile against 3.9 ms of actual work. + private static let scratchKey = "com.mapconductor.tilepng.scratch" + + private static func scratch(_ byteCount: Int) -> NSMutableData { + let dictionary = Thread.current.threadDictionary + if let existing = dictionary[scratchKey] as? NSMutableData, + existing.length >= byteCount { + return existing + } + let fresh = NSMutableData(length: byteCount)! + dictionary[scratchKey] = fresh + return fresh + } + + /// Encodes an image. Returns nil if the native path declined it, in which + /// case use `UIImage.pngData()`. + public static func encode(_ image: UIImage) -> Data? { + guard let cgImage = image.cgImage else { return nil } + let width = cgImage.width + let height = cgImage.height + guard width > 0, height > 0 else { return nil } + + let bytesPerRow = width * 4 + let pixels = scratch(bytesPerRow * height) + let base = pixels.mutableBytes + + // Drawn into a context we own rather than read out of the CGImage: a + // CGImage's backing store may be any layout CoreGraphics chose, and + // guessing at one is how a fast encoder produces wrong colours. + guard let context = CGContext( + data: base, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return nil } + // The buffer is reused, so anything the previous tile left behind has + // to go — a partly transparent tile would otherwise show the last one + // through its gaps. + context.clear(CGRect(x: 0, y: 0, width: width, height: height)) + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + + return encode(rgba: base, width: width, height: height, premultiplied: true) + } + + /** + Encodes RGBA8 pixels in place. + + Set `premultiplied` when the colour channels are scaled by alpha — a + CoreGraphics context's are, a Metal readback's usually are not. PNG stores + straight alpha, so encoding premultiplied pixels unchanged darkens every + partly transparent one. The buffer is un-premultiplied in place when this + is set, so its contents are not reusable afterwards. + + - Parameter rgba: `width * height * 4` writable bytes, rows top-down. + */ + public static func encode( + rgba: UnsafeMutableRawPointer, + width: Int, + height: Int, + premultiplied: Bool + ) -> Data? { + guard width > 0, height > 0 else { return nil } + + var outPointer: UnsafeMutablePointer? + var outLength = 0 + let status = tile_png_encode( + rgba.assumingMemoryBound(to: UInt8.self), + UInt32(width), + UInt32(height), + premultiplied ? 1 : 0, + &outPointer, + &outLength + ) + + guard status == ok, let outPointer else { return nil } + defer { tile_png_buffer_free(outPointer, outLength) } + return Data(bytes: outPointer, count: outLength) + } +} diff --git a/Tests/MapConductorCoreTests/ApplyStyleMutationsTests.swift b/Tests/MapConductorCoreTests/ApplyStyleMutationsTests.swift new file mode 100644 index 0000000..78ce76a --- /dev/null +++ b/Tests/MapConductorCoreTests/ApplyStyleMutationsTests.swift @@ -0,0 +1,221 @@ +import XCTest + +@testable import MapConductorCore + +/** + The body of every provider's mutation support, against a renderer that only + records. + + Kept in step with `ApplyStyleMutationsTest` on Android and + `applyStyleMutations.test.mjs` on the web. + */ +final class ApplyStyleMutationsTests: XCTestCase { + private final class Recorder: StyleMutationTarget { + var calls: [String] = [] + var layers: Set = ["roads", "labels"] + var refuse: String? + var ready = true + + var isReady: Bool { ready } + + func hasLayer(_ layerId: String) -> Bool { layers.contains(layerId) } + + func setPaint(_ layerId: String, _ key: String, _ valueJson: String) throws { + if key == refuse { throw Failure.refused } + calls.append("paint \(layerId) \(key)=\(valueJson)") + } + + func setLayout(_ layerId: String, _ key: String, _ valueJson: String) throws { + calls.append("layout \(layerId) \(key)=\(valueJson)") + } + + func setFilter(_ layerId: String, _ filterJson: String) throws { + calls.append("filter \(layerId)=\(filterJson)") + } + + func setZoomRange(_ layerId: String, _ minZoom: Double, _ maxZoom: Double) throws { + calls.append("zoom \(layerId)=\(minZoom)..\(maxZoom)") + } + + func addLayer(_ layerId: String, _ layerJson: String, _ beforeId: String?) throws { + calls.append("add \(layerId) \(layerJson) before=\(beforeId ?? "nil")") + } + + func removeLayer(_ layerId: String) throws { + layers.remove(layerId) + calls.append("remove \(layerId)") + } + + enum Failure: Error { case refused } + } + + func testSetsPaintAndLayoutProperties() { + let target = Recorder() + let unapplied = applyStyleMutations( + target, + [ + .setPaint(layerId: "roads", key: "line-color", value: "\"#fff\"", previous: "\"#888\""), + .setLayout(layerId: "labels", key: "visibility", value: "\"none\"", previous: "null"), + ] + ) + + XCTAssertTrue(unapplied.isEmpty) + XCTAssertEqual( + target.calls, + ["paint roads line-color=\"#fff\"", "layout labels visibility=\"none\""] + ) + } + + /// Every renderer takes both ends of a zoom range at once, so a mutation + /// that changes one has to restate the other — and a layer that named + /// neither gets the spec's own defaults. + func testAZoomRangeRestatesBothEnds() { + let target = Recorder() + _ = applyStyleMutations( + target, + [ + .setZoomRange( + layerId: "roads", minZoom: 5, maxZoom: nil, + previousMinZoom: nil, previousMaxZoom: nil + ), + ] + ) + XCTAssertEqual(target.calls, ["zoom roads=5.0..24.0"]) + } + + /// A layer the style does not have is not a crash and not a silent pass: + /// it comes back, and the caller says so. + func testWhatTheStyleHasNoLayerForComesBack() { + let target = Recorder() + let unapplied = applyStyleMutations( + target, + [ + .setPaint(layerId: "roads", key: "line-color", value: "\"#fff\"", previous: "null"), + .setPaint(layerId: "nope", key: "line-color", value: "\"#fff\"", previous: "null"), + ] + ) + XCTAssertEqual(unapplied.count, 1) + XCTAssertEqual(unapplied.first?.layerId, "nope") + XCTAssertEqual(target.calls.count, 1, "the good one still went through") + } + + /// One property the renderer rejects must not cost the other three hundred. + func testAThrowingPropertyIsReportedNotPropagated() { + let target = Recorder() + target.refuse = "line-gradient" + let unapplied = applyStyleMutations( + target, + [ + .setPaint(layerId: "roads", key: "line-gradient", value: "[]", previous: "null"), + .setPaint(layerId: "roads", key: "line-color", value: "\"#fff\"", previous: "null"), + ] + ) + XCTAssertEqual(unapplied.count, 1) + XCTAssertEqual(target.calls, ["paint roads line-color=\"#fff\""]) + } + + func testAddsAndRemovesALayer() { + let target = Recorder() + _ = applyStyleMutations( + target, + [.addLayer(layerId: "bg", layer: #"{"id":"bg","type":"background"}"#, beforeId: "roads")] + ) + XCTAssertEqual(target.calls, [#"add bg {"id":"bg","type":"background"} before=roads"#]) + + target.calls = [] + _ = applyStyleMutations(target, [.removeLayer(layerId: "roads")]) + XCTAssertEqual(target.calls, ["remove roads"]) + } + + /** + Adding and removing go through whatever the layer is, every time. + + The renderer decides what to do about a layer that is already there, or + already gone — only it can say. Putting the whole set back after a style + reload depends on this: a layer missing from the reloaded style must + still be added, and `hasLayer` cannot be trusted to notice. + */ + func testAddAndRemoveAreNotGuardedHere() { + let target = Recorder() + let unapplied = applyStyleMutations( + target, + [ + .addLayer(layerId: "roads", layer: #"{"id":"roads","type":"background"}"#, beforeId: nil), + .removeLayer(layerId: "gone"), + ] + ) + XCTAssertTrue(unapplied.isEmpty) + XCTAssertEqual( + target.calls, + [#"add roads {"id":"roads","type":"background"} before=nil"#, "remove gone"] + ) + } + + /** + No style yet is "not done" rather than "cannot be done". + + Conflating the two is what told an app "N adjustments were not applied: + this map cannot set those properties" about a map that was merely still + loading — and under the reloading policy it then handed the map the + adjusted document, which is the reload this design exists to avoid. + Nothing is lost by waiting: the store puts the whole set back when the + style loads. + */ + func testNothingIsReportedWhileThereIsNoStyle() { + let target = Recorder() + target.ready = false + + let unapplied = applyStyleMutations( + target, + [ + .setPaint(layerId: "roads", key: "line-color", value: "\"#fff\"", previous: "null"), + .setPaint(layerId: "nope", key: "line-color", value: "\"#fff\"", previous: "null"), + ] + ) + + XCTAssertTrue(unapplied.isEmpty) + XCTAssertTrue(target.calls.isEmpty, "not even attempted") + } + + /// Once it is ready, the judgement comes back exactly as before. + func testTheJudgementComesBackOnceReady() { + let target = Recorder() + let paint = StyleMutation.setPaint( + layerId: "nope", key: "line-color", value: "\"#fff\"", previous: "null") + + target.ready = false + XCTAssertTrue(applyStyleMutations(target, [paint]).isEmpty) + + target.ready = true + XCTAssertEqual(applyStyleMutations(target, [paint]).count, 1) + } + + /** + The default is "always ready", so a driver outside this repo that has + not implemented it behaves as it did before — a compatibility default, + not a recommendation. + */ + func testIsReadyDefaultsToTrue() { + struct Plain: StyleMutationTarget { + func hasLayer(_ layerId: String) -> Bool { false } + func setPaint(_ layerId: String, _ key: String, _ valueJson: String) throws {} + func setLayout(_ layerId: String, _ key: String, _ valueJson: String) throws {} + func setFilter(_ layerId: String, _ filterJson: String) throws {} + func setZoomRange(_ layerId: String, _ minZoom: Double, _ maxZoom: Double) throws {} + func addLayer(_ layerId: String, _ layerJson: String, _ beforeId: String?) throws {} + func removeLayer(_ layerId: String) throws {} + } + + XCTAssertTrue(Plain().isReady) + XCTAssertEqual( + applyStyleMutations( + Plain(), + [ + .setPaint( + layerId: "roads", key: "line-color", value: "\"#fff\"", previous: "null") + ] + ).count, + 1 + ) + } +} diff --git a/Tests/MapConductorCoreTests/DeviceSeamReproTests.swift b/Tests/MapConductorCoreTests/DeviceSeamReproTests.swift new file mode 100644 index 0000000..3bb999c --- /dev/null +++ b/Tests/MapConductorCoreTests/DeviceSeamReproTests.swift @@ -0,0 +1,128 @@ +import UIKit +import XCTest + +@testable import MapConductorCore + +/// 実機で出た「境界で左右の色が違う割れた丸」の再現。 +/// +/// 実機の街路樹と同じ規模・同じ設定(144,183 本、アイコン 101 色 20px、 +/// scale 2.8 相当、declutter 14、タイル 512px、z17)で隣接ペアを描き、 +/// 境界の左右 ±3px の帯を突き合わせる。どちらか片方だけに不透明画素があり、 +/// かつ左右の色相が食い違う行が続けば「割れ」。 +final class DeviceSeamReproTests: XCTestCase { + + private struct Lcg { + private var seed: UInt64 = 12345 + mutating func next() -> Double { + seed = seed &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407 + return Double(seed >> 11) / Double(UInt64(1) << 53) + } + } + + private func palette(_ count: Int) -> [ImageIcon] { + (0...defaultManager(minMarkerCount: 1) + let icons = palette(101) + var lcg = Lcg() + for at in 0..<144_183 { + let lat = 35.5 + lcg.next() * 0.4 + let lng = 139.5 + lcg.next() * 0.5 + manager.registerEntity( + MarkerEntity( + marker: nil, + state: MarkerState( + position: GeoPoint(latitude: lat, longitude: lng), + id: String(at), + icon: icons[at % icons.count] + ), + visible: true, isRendered: true, tiling: true + ) + ) + } + let renderer = MarkerTileRenderer( + markerManager: manager, + tileSize: 512, + cacheSizeBytes: 64 * 1024 * 1024, + iconScaleCallback: { _, z in (z > 15 ? 1.4 : 1.0) * 2.0 }, + declutterPx: 14 + ) + + func rgba(_ png: Data) throws -> (pixels: [UInt8], width: Int, height: Int) { + let image = try XCTUnwrap(UIImage(data: png)?.cgImage) + var pixels = [UInt8](repeating: 0, count: image.width * image.height * 4) + let context = CGContext( + data: &pixels, width: image.width, height: image.height, + bitsPerComponent: 8, bytesPerRow: image.width * 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + context?.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) + return (pixels, image.width, image.height) + } + + let z = 17 + let n = Double(1 << z) + let latRad = 35.69 * Double.pi / 180 + let cx = Int((139.694 + 180.0) / 360.0 * n) + let cy = Int((1.0 - log(tan(latRad) + 1.0 / cos(latRad)) / .pi) / 2.0 * n) + + var seams = 0 + var badSeams = 0 + for dx in -3...3 { + for dy in -3...3 { + let left = renderer.renderTile(request: TileRequest(x: cx + dx, y: cy + dy, z: z)) + let right = renderer.renderTile(request: TileRequest(x: cx + dx + 1, y: cy + dy, z: z)) + guard let left, let right else { continue } + let l = try rgba(left) + let r = try rgba(right) + seams += 1 + // 境界の両側 3px: 左タイルの最終 3 列 vs 右タイルの先頭 3 列は + // **同じ world 列ではない**が、境界をまたぐ円は両方に描かれる。 + // 割れの判定は「左の最終列に不透明があるのに、右の先頭列の同じ行が + // 透明、または色相が大きく違う」が 6 行以上連続。 + var run = 0 + var worst = 0 + for row in 0.. 200, ra > 200 { + let dr = abs(Int(l.pixels[li]) - Int(r.pixels[ri])) + let dg = abs(Int(l.pixels[li + 1]) - Int(r.pixels[ri + 1])) + let db = abs(Int(l.pixels[li + 2]) - Int(r.pixels[ri + 2])) + broken = (dr + dg + db) > 180 + } + run = broken ? run + 1 : 0 + worst = max(worst, run) + } + if worst >= 4 { + badSeams += 1 + print("SEAMREPRO z=\(z) between x=\(cx + dx),\(cx + dx + 1) y=\(cy + dy) worstRun=\(worst)") + for (name, data) in [("L", left), ("R", right)] { + let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.png") + attachment.name = "bad-\(cx + dx)-\(cy + dy)-\(name)" + attachment.lifetime = .keepAlways + add(attachment) + } + } + } + } + print("SEAMREPRO seams=\(seams) bad=\(badSeams)") + XCTAssertEqual(badSeams, 0, "\(badSeams)/\(seams) の境界で左右が食い違う") + } +} diff --git a/Tests/MapConductorCoreTests/GeocellConformanceTests.swift b/Tests/MapConductorCoreTests/GeocellConformanceTests.swift new file mode 100644 index 0000000..465aeb4 --- /dev/null +++ b/Tests/MapConductorCoreTests/GeocellConformanceTests.swift @@ -0,0 +1,100 @@ +import XCTest + +@testable import MapConductorCore + +/// geocell の適合テスト。 +/// +/// 同じ `geocell-vectors.txt` を android-sdk / react-sdk も読む。3 つが同じセルを +/// 名指すことを担保するのが目的で、ここが見ているのはこのプラットフォームの分だけ。 +/// +/// `ProjectionConformanceTests` と同じ作りにしてある。違うのは中身ではなく、 +/// **なぜ文字列で比べるか**のほう。投影は 3 つとも Double を返すので桁を丸めれば +/// 済むが、セルの鍵は Long / Int64 / number と型が違う。JS の `number` は 53 ビット +/// までしか整数を持てず、ビット演算にいたっては 32 ビットに丸められる。数値を +/// そのまま突き合わせると、揃っていないのか表現が違うだけなのか区別できない。 +/// 0123456789abcdef の文字列にしてしまえば、その問いは消える。 +final class GeocellConformanceTests: XCTestCase { + + private func runVectors() throws -> String { + guard let url = Bundle.module.url(forResource: "geocell-vectors", withExtension: "txt") else { + XCTFail("geocell-vectors.txt が見つからない") + return "" + } + var out = "" + for line in try String(contentsOf: url, encoding: .utf8).split(separator: "\n", omittingEmptySubsequences: false) { + let row = line.trimmingCharacters(in: .whitespaces) + if row.isEmpty || row.hasPrefix("#") { continue } + let parts = row.split(separator: "|", omittingEmptySubsequences: false).map(String.init) + switch parts[0] { + case "geocell": + let text = MarkerGrid.geocell( + latitude: Double(parts[1])!, + longitude: Double(parts[2])!, + characters: Int(parts[3])! + ) + out += "geocell|\(parts[1])|\(parts[2])|\(parts[3])|\(text)\n" + case "level": + let level = MarkerGrid.level(forSeparation: Double(parts[1])!) + out += "level|\(parts[1])|\(level.map(String.init) ?? "none")\n" + case "cell": + let level = Int64(parts[3])! + let lat = MarkerGrid.latCell(Double(parts[1])!, level: level) + let lon = MarkerGrid.lonCell(Double(parts[2])!, level: level) + out += "cell|\(parts[1])|\(parts[2])|\(parts[3])|\(lat),\(lon)\n" + case "qlevel": + let level = MarkerGrid.queryLevel(latSpan: Double(parts[1])!, lonSpan: Double(parts[2])!) + out += "qlevel|\(parts[1])|\(parts[2])|\(level)\n" + case "colwalk": + let level = Int64(parts[3])! + let span = MarkerGrid.eastwardSpan(from: Double(parts[1])!, to: Double(parts[2])!) + let walk = MarkerGrid.columnWalk(from: Double(parts[1])!, spanning: span, level: level) + out += "colwalk|\(parts[1])|\(parts[2])|\(parts[3])|\(walk.start),\(walk.count)\n" + case "morton": + let key = MarkerGrid.morton( + Int64(parts[1])!, Int64(parts[2])!, level: Int64(parts[3])! + ) + out += "morton|\(parts[1])|\(parts[2])|\(parts[3])|\(key)\n" + default: + XCTFail("未知の演算: \(parts[0])") + } + } + return out + } + + func testVectorsAllRun() throws { + let actual = try runVectors() + + // 突き合わせに使う。テストの副産物ではなく、これが成果物。 + let destination = FileManager.default.temporaryDirectory + .appendingPathComponent("geocell-actual.ios.txt") + try actual.write(to: destination, atomically: true, encoding: .utf8) + print("GEOCELL_CONFORMANCE_BEGIN") + print(actual, terminator: "") + print("GEOCELL_CONFORMANCE_END") + + XCTAssertFalse(actual.split(separator: "\n").isEmpty, "ベクタが 1 行も処理されていない") + } + + func testMatchesCanonicalIfPresent() throws { + guard let url = Bundle.module.url(forResource: "geocell-expected", withExtension: "txt") else { + throw XCTSkip("正本がまだない") + } + XCTAssertEqual(try String(contentsOf: url, encoding: .utf8), try runVectors()) + } + + /// 深い段の答えは、浅い段の答えを接頭辞に持つ。 + /// + /// 索引が階層として働く条件そのもので、これが崩れていると「粗いセルの + /// マーカーは連続している」という前提が外れ、範囲クエリが静かに取りこぼす。 + func testDeeperCellsExtendShallowerOnes() { + for (latitude, longitude) in [(35.681236, 139.767125), (-33.86882, 151.20929), (0.0, 0.0)] { + var previous = "" + for characters in 1...10 { + let text = MarkerGrid.geocell(latitude: latitude, longitude: longitude, characters: characters) + XCTAssertEqual(text.count, characters) + XCTAssertTrue(text.hasPrefix(previous), "\(text) が \(previous) を接頭辞に持たない") + previous = text + } + } + } +} diff --git a/Tests/MapConductorCoreTests/IconSizeProbeTests.swift b/Tests/MapConductorCoreTests/IconSizeProbeTests.swift new file mode 100644 index 0000000..baaf36c --- /dev/null +++ b/Tests/MapConductorCoreTests/IconSizeProbeTests.swift @@ -0,0 +1,78 @@ +import UIKit +import XCTest + +@testable import MapConductorCore + +/// アイコンが実際に何ピクセルで描かれるかを、出来上がったタイルから数える。 +/// +/// android-sdk の `IconSizeProbeTest` と対になっていて、同じ 14 単位の円を、 +/// 同じ zoom・同じ iconScaleCallback で描いて比べる。計算で追うと前提を一つ +/// 間違えるだけで答えが変わるので、描いた結果を測る。 +final class IconSizeProbeTests: XCTestCase { + + func testDrawnIconSize() { + let screenScale = UIScreen.main.scale + // プロバイダと同じ組み立て(GoogleMapMarkerController.setupTileRenderer): + // タイルは Retina 倍、コールバックも画面スケール倍して渡す。 + let tileSize = 256 * max(1, Int(screenScale)) + let contentScale = Double(screenScale) + let callbackScale = 1.4 // サンプルの zoom > 15 の帯 + + // サンプルと同じ作り: 14 ポイントの円。 + let sizePt: CGFloat = 10 + let image = UIGraphicsImageRenderer(size: CGSize(width: sizePt, height: sizePt)).image { ctx in + UIColor.red.setFill() + ctx.cgContext.fillEllipse(in: CGRect(x: 0.5, y: 0.5, width: sizePt - 1, height: sizePt - 1)) + } + let icon = ImageIcon(image: image, iconSize: sizePt) + + let zoom = 16 + let n = Double(1 << zoom) + let lat = 35.68, lon = 139.75 + let latRad = lat * .pi / 180 + let tx = Int((lon + 180.0) / 360.0 * n) + let ty = Int((1.0 - log(tan(latRad) + 1.0 / cos(latRad)) / .pi) / 2.0 * n) + + let manager = MarkerManager.defaultManager(minMarkerCount: 1) + manager.registerEntity( + MarkerEntity( + marker: nil, + state: MarkerState(position: GeoPoint(latitude: lat, longitude: lon), icon: icon), + visible: true, isRendered: true, tiling: true + ) + ) + let renderer = MarkerTileRenderer( + markerManager: manager, + tileSize: tileSize, + cacheSizeBytes: 1 << 20, + iconScaleCallback: { _, _ in callbackScale * contentScale } + ) + + guard let data = renderer.renderTile(request: TileRequest(x: tx, y: ty, z: zoom)), + let cg = UIImage(data: data)?.cgImage else { + return XCTFail("タイルが描けていない") + } + + let w = cg.width, h = cg.height + var pixels = [UInt8](repeating: 0, count: w * h * 4) + CGContext( + data: &pixels, width: w, height: h, bitsPerComponent: 8, bytesPerRow: w * 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + )!.draw(cg, in: CGRect(x: 0, y: 0, width: w, height: h)) + + var minX = w, maxX = -1 + for y in 0.. 8 { + minX = min(minX, x); maxX = max(maxX, x) + } + } + let drawnPx = maxX - minX + 1 + // タイルは 256pt として表示されるので、見かけの大きさはこの比。 + let apparentPt = Double(drawnPx) / Double(w) * 256.0 + print(String( + format: "ICONPROBE ios scale=%.2f tile=%d canvas=%d iconSize=%.0f drawnPx=%d apparentPt=%.2f", + screenScale, tileSize, w, sizePt, drawnPx, apparentPt)) + XCTAssertGreaterThan(drawnPx, 0) + } +} diff --git a/Tests/MapConductorCoreTests/KDTreeRadiusTests.swift b/Tests/MapConductorCoreTests/KDTreeRadiusTests.swift new file mode 100644 index 0000000..bc31d9e --- /dev/null +++ b/Tests/MapConductorCoreTests/KDTreeRadiusTests.swift @@ -0,0 +1,45 @@ +import CoreGraphics +import XCTest + +@testable import MapConductorCore + +/** + The unordered radius search returns the same cells as the sorted one. + + It exists because sorting dominated bounds queries — 42 ms of a 64 ms query at + 20k markers — and bounds queries throw the distances away. Its traversal was + written out separately, so this pins the two against each other rather than + trusting that the pruning was copied correctly. + */ +final class KDTreeRadiusTests: XCTestCase { + + private func cells(_ count: Int) -> [HexCell] { + var seed: UInt64 = 42 + return (0..> 40) / 1000.0 + seed = seed &* 6364136223846793005 &+ 1442695040888963407 + let y = Double(seed >> 40) / 1000.0 + return HexCell( + coord: HexCoord(q: index, r: index), + centerLatLng: GeoPoint(latitude: 0, longitude: 0), + centerXY: CGPoint(x: x, y: y), + id: "cell-\(index)" + ) + } + } + + func testUnorderedMatchesSorted() { + let tree = KDTree(points: cells(2_000)) + + // A spread of radii: one that excludes everything, several partial, and + // one that takes the lot. A traversal bug that only shows at the + // boundary would survive a single radius. + for radius in [0.0, 1.0, 50.0, 500.0, 5_000.0, 20_000.0] { + let query = CGPoint(x: 8_000, y: 8_000) + let sorted = Set(tree.withinRadiusWithDistance(query: query, radius: radius).map { $0.cell.id }) + let unordered = Set(tree.withinRadius(query: query, radius: radius).map { $0.id }) + XCTAssertEqual(sorted, unordered, "radius \(radius): \(sorted.count) vs \(unordered.count)") + } + } +} diff --git a/Tests/MapConductorCoreTests/LocalTileServerAbandonTests.swift b/Tests/MapConductorCoreTests/LocalTileServerAbandonTests.swift new file mode 100644 index 0000000..b4f9e37 --- /dev/null +++ b/Tests/MapConductorCoreTests/LocalTileServerAbandonTests.swift @@ -0,0 +1,190 @@ +import XCTest +@testable import MapConductorCore + +/// 描いている途中で見捨てられたタイルに、こちらが気づけるか。 +/// +/// 気づけないと、地図が興味を失ったタイルを最後まで描き切る。1 枚なら些細だが、 +/// ArcGIS の 3D は iPad の 1 画面に 119 枚を要求し、描画は 6 並列。**見られない +/// タイルが、見られるタイルの前に並ぶ**ぶんだけ画面が埋まるのが遅れる。 +/// +/// android-sdk はソケットを覗いて EOF を見る。iOS は `NWConnection` の +/// `state` を見ている——ここが本当に動くのかを固定するのがこのテスト。 +final class LocalTileServerAbandonTests: XCTestCase { + + private let routeId = "abandon-test" + private var server: LocalTileServer! + + override func setUp() { + super.setUp() + server = TileServerRegistry.get() + } + + override func tearDown() { + server.unregister(routeId: routeId) + super.tearDown() + } + + /// 描くのに時間がかかり、その間ずっと「まだ要る?」を聞き続けるプロバイダ。 + private final class SlowProvider: TileProvider, @unchecked Sendable { + let started = DispatchSemaphore(value: 0) + private let lock = NSLock() + private var _noticed = false + private var _asked = 0 + + /// 中断に気づいたか。 + var noticed: Bool { + lock.lock(); defer { lock.unlock() } + return _noticed + } + + /// 何回聞いたか(0 なら、そもそも聞く前に終わっている)。 + var asked: Int { + lock.lock(); defer { lock.unlock() } + return _asked + } + + func renderTile(request: TileRequest) -> Data? { nil } + + func renderTile(request: TileRequest, isCancelled: () -> Bool) throws -> Data? { + started.signal() + // 5 秒ぶん、20ms ごとに聞く。クライアントが切れた時点で気づけるなら + // この間に true になるはず。 + for _ in 0..<250 { + lock.lock() + _asked += 1 + lock.unlock() + if isCancelled() { + lock.lock(); _noticed = true; lock.unlock() + return nil + } + Thread.sleep(forTimeInterval: 0.02) + } + return TransparentTile.png(size: 8) + } + } + + private final class CancellationFlag: @unchecked Sendable { + private let lock = NSLock() + private var value = false + + func cancel() { + lock.lock(); value = true; lock.unlock() + } + + var isCancelled: Bool { + lock.lock(); defer { lock.unlock() } + return value + } + } + + private final class FixedProvider: TileProvider { + let png: Data + + init(png: Data) { self.png = png } + + func renderTile(request: TileRequest) -> Data? { png } + } + + private func tileURL() throws -> URL { + let text = server.urlTemplate(routeId: routeId, tileSize: 256) + .replacingOccurrences(of: "{z}", with: "10") + .replacingOccurrences(of: "{x}", with: "1") + .replacingOccurrences(of: "{y}", with: "2") + return try XCTUnwrap(URL(string: text)) + } + + /// ArcGIS's CustomTiledLayer exposes real task cancellation, so the + /// in-process route must pass it through to the provider immediately. + func testDirectTileNoticesAuthoritativeCancellation() throws { + let provider = SlowProvider() + let flag = CancellationFlag() + server.register(routeId: routeId, provider: provider) + let url = try tileURL() + let finished = expectation(description: "direct render stopped") + + DispatchQueue.global().async { + let data = self.server.renderLocalTile(url: url) { flag.isCancelled } + XCTAssertNil(data) + finished.fulfill() + } + + XCTAssertEqual(provider.started.wait(timeout: .now() + 10), .success) + flag.cancel() + wait(for: [finished], timeout: 1) + XCTAssertTrue(provider.noticed) + XCTAssertLessThan(provider.asked, 50, "キャンセル後も長く描画を続けた") + } + + func testDirectTileReturnsProviderBytes() throws { + let png = try XCTUnwrap(TransparentTile.png(size: 8)) + server.register(routeId: routeId, provider: FixedProvider(png: png)) + + XCTAssertEqual(server.renderLocalTile(url: try tileURL()), png) + } + + func testDirectTileRejectsAnotherServerURL() throws { + server.register( + routeId: routeId, + provider: FixedProvider(png: try XCTUnwrap(TransparentTile.png(size: 8))) + ) + + let foreign = try XCTUnwrap(URL(string: "http://example.com/tiles/\(routeId)/256/10/1/2.png")) + XCTAssertNil(server.renderLocalTile(url: foreign)) + } + + /** + クライアントが途中で帰ったら、描くのをやめられること。 + + **HTTP 経路は現状これを満たしていない。** `NWConnection` の `state` は + **相手が閉じても変わらない**(自分が cancel したときと、エラーのときだけ)ので、 + リクエストを解析してから応答を書くまでの間、受信は 1 つも張られていない。 + 結果、地図が見捨てたタイルも最後まで描き切る。 + + ArcGIS だけはこの穴を迂回済み。`renderLocalTile` を `CustomTiledLayer` の + クロージャから直接呼び、ArcGIS 自身のタスクキャンセルを権威として使う + (`testDirectTileNoticesAuthoritativeCancellation` が固定している)。 + HTTP で読む他のプロバイダ(MapKit, Google Maps ほか)はまだこの穴の中にいる。 + + 描画中に受信を張って EOF を見る、という素直な直し方を試したところ**壊れた**: + HTTP クライアントはリクエストを送り終えた時点で書き込み側を閉じることがあり、 + それが `isComplete` として即座に届く。「もう要らない」ではなく「送り終えた」 + なので、そこで接続を切ると応答を書く前に切ることになる(実機で 3 枚しか + 描けず地図が白くなった)。android がソケットを覗いているのは、この 2 つを + 区別するためだと思われる。 + + 直すまで期待値として記録しておく。消すとこの穴は誰にも見えなくなる。 + */ + func testNoticesAClientThatGaveUpMidRender() throws { + XCTExpectFailure("見捨てられたタイルに気づけない。上の説明を参照") + let provider = SlowProvider() + server.register(routeId: routeId, provider: provider) + + let url = try tileURL() + + let configuration = URLSessionConfiguration.ephemeral + configuration.urlCache = nil + let session = URLSession(configuration: configuration) + let task = session.dataTask(with: url) { _, _, _ in } + task.resume() + + // 描き始めてから帰る。始まる前に切ると、中断ではなく「来なかった」に + // なってしまい、確かめたいものが確かめられない。 + XCTAssertEqual( + provider.started.wait(timeout: .now() + 10), .success, + "プロバイダが呼ばれない" + ) + task.cancel() + + // 気づくなら即座のはず。長めに待ってから判定する。 + let deadline = Date().addingTimeInterval(3) + while Date() < deadline && !provider.noticed { + Thread.sleep(forTimeInterval: 0.05) + } + + XCTAssertGreaterThan(provider.asked, 0, "isCancelled が一度も呼ばれていない") + XCTAssertTrue( + provider.noticed, + "クライアントが帰ったのに描き続けている(\(provider.asked) 回聞いて一度も true にならない)" + ) + } +} diff --git a/Tests/MapConductorCoreTests/LocalTileServerSemanticsTests.swift b/Tests/MapConductorCoreTests/LocalTileServerSemanticsTests.swift new file mode 100644 index 0000000..956d6e9 --- /dev/null +++ b/Tests/MapConductorCoreTests/LocalTileServerSemanticsTests.swift @@ -0,0 +1,193 @@ +import XCTest +@testable import MapConductorCore + +/// サーバが「タイルが無い」理由をどう答えるか。 +/// +/// 3 通りあり、取り違えると画面に出る。地図 SDK は 404 を**恒久の答え**として +/// 覚えるので、一瞬だけ空だったタイル(データ取り込み中、レイヤ切り替えの最中)を +/// 404 で答えると二度と要求されず、隣のタイルのはみ出し分だけが残って穴の縁で +/// アイコンが半分に切れる。空は透明な絵、失敗は引き直せる 503、404 は +/// 「その名前のものは無い」ときだけ。 +/// +/// android-sdk-core の `LocalTileServerSemanticsTest` と同じ 4 点を見ている。 +final class LocalTileServerSemanticsTests: XCTestCase { + + private let routeId = "semantics-test" + private var server: LocalTileServer! + + /// キャッシュを持たないセッション。 + /// + /// 共有セッションで撮ると、空タイルに付く `max-age=31536000` のせいで + /// 2 本目以降が**前のテストの答え**を受け取る(実際に一度そうなった)。 + /// 見たいのはサーバが今なんと答えるかなので、毎回本当に聞きに行く。 + private lazy var session: URLSession = { + let configuration = URLSessionConfiguration.ephemeral + configuration.urlCache = nil + configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData + return URLSession(configuration: configuration) + }() + + override func setUp() { + super.setUp() + server = TileServerRegistry.get() + } + + override func tearDown() { + server.unregister(routeId: routeId) + super.tearDown() + } + + /// 空で nil を返すプロバイダ。geojson / kml / groundimage と同じ形。 + private final class EmptyProvider: TileProvider { + func renderTile(request: TileRequest) -> Data? { nil } + } + + /// 今は描けないプロバイダ。 + private final class FailingProvider: TileProvider { + struct Failure: Error {} + + func renderTile(request: TileRequest) -> Data? { nil } + + func renderTile(request: TileRequest, isCancelled: () -> Bool) throws -> Data? { + throw Failure() + } + } + + private final class FixedProvider: TileProvider { + let png: Data + + init(png: Data) { self.png = png } + + func renderTile(request: TileRequest) -> Data? { png } + } + + private final class RequestProvider: TileProvider { + var request: TileRequest? + func renderTile(request: TileRequest) -> Data? { + self.request = request + return nil + } + } + + func testSourceDetailOverrideReachesDirectAndHTTPProviders() throws { + let provider = RequestProvider() + server.register(routeId: routeId, provider: provider) + let template = server.urlTemplate(routeId: routeId, tileSize: 256) + let path = template.replacingOccurrences(of: "{z}", with: "8") + .replacingOccurrences(of: "{x}", with: "226") + .replacingOccurrences(of: "{y}", with: "100") + let url = try XCTUnwrap(URL(string: path + "?sourceZoomOffset=1")) + XCTAssertNotNil(server.renderLocalTile(url: url)) + XCTAssertEqual(provider.request?.sourceZoomOffset, 1) + XCTAssertEqual(provider.request?.z, 8) + _ = try fetch(z: 8, x: 226, y: 100, sourceZoomOffset: 2) + XCTAssertEqual(provider.request?.sourceZoomOffset, 2) + _ = try fetch(z: 8, x: 226, y: 100) + XCTAssertEqual(provider.request?.sourceZoomOffset, 0) + } + + private struct Answer { + let status: Int + let headers: [AnyHashable: Any] + let body: Data + } + + /// タイルを 1 枚取りに行く。`urlTemplate` が返す URL をそのまま使う。 + private func fetch(z: Int, x: Int, y: Int, tileSize: Int = 512, retina: Bool = false, sourceZoomOffset: Int? = nil) throws -> Answer { + let template = server.urlTemplate(routeId: routeId, tileSize: tileSize) + var text = template + .replacingOccurrences(of: "{z}", with: "\(z)") + .replacingOccurrences(of: "{x}", with: "\(x)") + .replacingOccurrences(of: "{y}", with: "\(y)") + if retina { + text = text.replacingOccurrences(of: ".png", with: "@2x.png") + } + if let sourceZoomOffset { text += "?sourceZoomOffset=\(sourceZoomOffset)" } + let url = try XCTUnwrap(URL(string: text)) + + var answer: Answer? + let done = expectation(description: "tile \(text)") + let task = session.dataTask(with: url) { data, response, _ in + if let http = response as? HTTPURLResponse { + answer = Answer(status: http.statusCode, headers: http.allHeaderFields, body: data ?? Data()) + } + done.fulfill() + } + task.resume() + wait(for: [done], timeout: 20) + return try XCTUnwrap(answer, "サーバが答えなかった") + } + + private func header(_ answer: Answer, _ name: String) -> String? { + for (key, value) in answer.headers where (key as? String)?.lowercased() == name.lowercased() { + return value as? String + } + return nil + } + + /// 登録されていないルートは「その名前のものは無い」。404 はここだけ。 + func testUnknownRouteIsNotFound() throws { + let answer = try fetch(z: 10, x: 0, y: 0) + XCTAssertEqual(answer.status, 404) + } + + /// 空は本物の答え。透明な絵で 200。 + /// + /// マーカーのレンダラだけでなく、空で nil を返すすべてのプロバイダに効く + /// ことを確かめたいので、いちばん素朴なプロバイダで見る。 + func testEmptyTileIsATransparentPicture() throws { + server.register(routeId: routeId, provider: EmptyProvider()) + let answer = try fetch(z: 10, x: 11, y: 12) + XCTAssertEqual(answer.status, 200) + XCTAssertEqual(header(answer, "Content-Type"), "image/png") + + let image = try XCTUnwrap(UIImage(data: answer.body), "PNG として読めない") + XCTAssertEqual(image.size.width, 512) + XCTAssertEqual(image.size.height, 512) + XCTAssertEqual(alpha(of: image, atX: 0, y: 0), 0, "隅が不透明") + XCTAssertEqual(alpha(of: image, atX: 256, y: 256), 0, "中央が不透明") + } + + /// @2x の URL には、その密度の透明タイルを返す。 + func testEmptyRetinaTileMatchesItsDensity() throws { + server.register(routeId: routeId, provider: EmptyProvider()) + let answer = try fetch(z: 10, x: 21, y: 22, retina: true) + XCTAssertEqual(answer.status, 200) + let image = try XCTUnwrap(UIImage(data: answer.body)) + XCTAssertEqual(image.size.width, 1024) + } + + /// 描けなかったタイルは 503。404 にすると穴が残り、200 にすると嘘が残る。 + func testRenderFailureIsRetryable() throws { + server.register(routeId: routeId, provider: FailingProvider()) + let answer = try fetch(z: 10, x: 31, y: 32) + XCTAssertEqual(answer.status, 503) + XCTAssertEqual(header(answer, "Retry-After"), "1") + XCTAssertEqual(header(answer, "Cache-Control"), "no-store") + } + + /// 描けたタイルはそのまま返る(上の 3 つが偶然通っていないことの確認)。 + func testRenderedTileIsServedAsIs() throws { + let png = try XCTUnwrap(TransparentTile.png(size: 8)) + server.register(routeId: routeId, provider: FixedProvider(png: png)) + let answer = try fetch(z: 10, x: 41, y: 42) + XCTAssertEqual(answer.status, 200) + XCTAssertEqual(answer.body, png) + } + + private func alpha(of image: UIImage, atX x: Int, y: Int) -> Int { + guard let cgImage = image.cgImage else { return -1 } + var pixel: [UInt8] = [0, 0, 0, 0] + guard let context = CGContext( + data: &pixel, + width: 1, + height: 1, + bitsPerComponent: 8, + bytesPerRow: 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return -1 } + context.draw(cgImage, in: CGRect(x: -x, y: -(cgImage.height - 1 - y), width: cgImage.width, height: cgImage.height)) + return Int(pixel[3]) + } +} diff --git a/Tests/MapConductorCoreTests/MapViewStyleHostTests.swift b/Tests/MapConductorCoreTests/MapViewStyleHostTests.swift new file mode 100644 index 0000000..08b2029 --- /dev/null +++ b/Tests/MapConductorCoreTests/MapViewStyleHostTests.swift @@ -0,0 +1,199 @@ +import XCTest + +@testable import MapConductorCore + +/** + The decision table of ``MapViewStyleHost/apply(_:onDiagnostics:)``. + + The only part of this class that carries a decision is when to install + again and when to leave things alone; the rest is handing things through to + a provider. And that condition was once inverted, unnoticed: **the same key + called `update`** (recompile → diagnostics → state → another `body` → + round again, which the user saw as the bottom-left panel flickering), and + **a changed key never called `update`** (dispose and install every time, + which is the reload the whole delta-compiling design exists to avoid). + Both are the kind of break that compiles and still draws a map, so the + table is written down here. + + Kept in step with android-sdk's `MapViewStyleHostTest` and js-sdk-core's + `MapViewStyleHost.test.mjs`. + */ +final class MapViewStyleHostTests: XCTestCase { + /// Records what it was asked to do, and nothing else. + private struct RecordingStyle: MapViewStyle { + let key: String + let calls: Calls + /// Whether `update` takes it. False means the caller disposes and + /// installs from scratch. A closure so the answer can change after + /// the style is installed (see `testAnAcceptedUpdateResetsTheRun`). + let takesUpdates: () -> Bool + + func install(host: MapStyleHost) -> MapStyleInstallation { + calls.record("install(\(key))") + let key = key + let calls = calls + let takesUpdates = takesUpdates + return MapStyleInstallation( + dispose: { calls.record("dispose(\(key))") }, + update: { next in + calls.record("update(\(key) -> \(next.key))") + return takesUpdates() + } + ) + } + } + + /// A reference box, because `MapViewStyle` is a value and the closures + /// it hands out are captured. + private final class Calls { + private(set) var recorded: [String] = [] + func record(_ call: String) { recorded.append(call) } + func clear() { recorded = [] } + } + + private var calls = Calls() + + override func setUp() { + super.setUp() + calls = Calls() + } + + private func host() -> MapViewStyleHost { + MapViewStyleHost(serviceRegistry: MutableMapServiceRegistry()) + } + + private func style(_ key: String, takesUpdates: Bool = true) -> RecordingStyle { + RecordingStyle(key: key, calls: calls, takesUpdates: { takesUpdates }) + } + + private func style(_ key: String, takesUpdates: @escaping () -> Bool) -> RecordingStyle { + RecordingStyle(key: key, calls: calls, takesUpdates: takesUpdates) + } + + func testTheFirstApplyInstalls() { + // Held in a `let`: the host disposes what it installed when it goes, + // so a temporary would install and immediately take it back off. + let host = host() + host.apply(style("a")) + XCTAssertEqual(calls.recorded, ["install(a)"]) + } + + /** + An unchanged key does nothing. Inverted, this was the flicker: the + recompile is not merely wasted work, it re-reports the diagnostics, and + an app that renders them sets state and asks for another `body`. + */ + func testAnUnchangedKeyDoesNothing() { + let host = host() + host.apply(style("a")) + calls.clear() + + // Different values, same content -- which is what a style built + // inline in a `body` produces on every evaluation. + host.apply(style("a")) + host.apply(style("a")) + + XCTAssertEqual(calls.recorded, []) + } + + /** + A changed key is offered to what is installed before anything is torn + down. A slider that does not reload the map is this one line. + */ + func testAChangedKeyIsOfferedBeforeBeingReinstalled() { + let host = host() + host.apply(style("a")) + calls.clear() + + host.apply(style("b")) + + XCTAssertEqual(calls.recorded, ["update(a -> b)"]) + } + + /// Once an update is taken, the new key is what "unchanged" means. + func testAnAcceptedKeyBecomesTheInstalledOne() { + let host = host() + host.apply(style("a")) + host.apply(style("b")) + calls.clear() + + host.apply(style("b")) + + XCTAssertEqual(calls.recorded, []) + } + + func testARefusedUpdateDisposesAndInstalls() { + let host = host() + host.apply(style("a", takesUpdates: false)) + calls.clear() + + host.apply(style("b", takesUpdates: false)) + + XCTAssertEqual(calls.recorded, ["update(a -> b)", "dispose(a)", "install(b)"]) + } + + /** + A style with no `update` of its own takes the protocol's default, + which is "refused". If that ever read as "took it", a new key would be + recorded against an installation that never saw it. + */ + func testAStyleThatCannotUpdateIsReinstalled() { + struct Plain: MapViewStyle { + let key: String + let calls: Calls + + func install(host: MapStyleHost) -> MapStyleInstallation { + calls.record("install(\(key))") + let key = key + let calls = calls + return MapStyleInstallation(dispose: { calls.record("dispose(\(key))") }) + } + } + let host = host() + host.apply(Plain(key: "a", calls: calls)) + calls.clear() + + host.apply(Plain(key: "b", calls: calls)) + + XCTAssertEqual(calls.recorded, ["dispose(a)", "install(b)"]) + } + + /// Nil takes it off: the app gets the map it had, not the one the style + /// left behind. + func testNilTakesItOff() { + let host = host() + host.apply(style("a")) + calls.clear() + + host.apply(nil) + XCTAssertEqual(calls.recorded, ["dispose(a)"]) + + // And the same key afterwards installs rather than being left alone. + calls.clear() + host.apply(style("a")) + XCTAssertEqual(calls.recorded, ["install(a)"]) + } + + /** + Only a *run* of refusals is a problem, so one that is taken starts the + count over. + + The diagnostic itself is not asserted here: ``MCLog`` writes to + `os_log` with no seam to read it back, unlike android-sdk's + `MapDiagnostics.sink`. What is checked is that the run is counted the + way the threshold assumes -- consecutively. + */ + func testAnAcceptedUpdateResetsTheRun() { + let host = host() + var takes = false + host.apply(style("k0", takesUpdates: { takes })) + for index in 0..<4 { host.apply(style("r\(index)", takesUpdates: { takes })) } + calls.clear() + + takes = true + host.apply(style("ok", takesUpdates: { takes })) + + // Taken in place: nothing torn down, nothing installed again. + XCTAssertEqual(calls.recorded, ["update(r3 -> ok)"]) + } +} diff --git a/Tests/MapConductorCoreTests/MarkerGridIndexTests.swift b/Tests/MapConductorCoreTests/MarkerGridIndexTests.swift new file mode 100644 index 0000000..d346361 --- /dev/null +++ b/Tests/MapConductorCoreTests/MarkerGridIndexTests.swift @@ -0,0 +1,289 @@ +import XCTest + +@testable import MapConductorCore + +/// `MarkerGridIndex` must answer exactly what a scan would. +/// +/// The index packs a cell key and an array position into one Int64 and reads +/// them back with shifts and masks, so an off-by-one in the bit layout or in a +/// range bound does not throw — it quietly drops markers. Every test here is +/// the same shape: ask the index, ask brute force, demand the same set. +final class MarkerGridIndexTests: XCTestCase { + + private func entity(_ id: Int, _ latitude: Double, _ longitude: Double) -> MarkerEntity { + MarkerEntity( + marker: id, + state: MarkerState( + position: GeoPoint(latitude: latitude, longitude: longitude), + id: String(id) + ), + isRendered: true + ) + } + + /// A deterministic spread, so a failure is reproducible. + private func scatter(count: Int, latitude: Double, longitude: Double, spread: Double) + -> [MarkerEntity] { + var generator = SystemRandomNumberGenerator() + _ = generator + var seed: UInt64 = 0x2545_F491_4F6C_DD1D + func next() -> Double { + seed = seed &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407 + return Double(seed >> 11) / Double(UInt64(1) << 53) + } + return (0..]) -> Set { + Set(entities.map(\.state.id)) + } + + private func bounds(_ south: Double, _ west: Double, _ north: Double, _ east: Double) + -> GeoRectBounds { + GeoRectBounds( + southWest: GeoPoint(latitude: south, longitude: west), + northEast: GeoPoint(latitude: north, longitude: east) + ) + } + + func testBoundsQueryMatchesBruteForce() { + let markers = scatter(count: 5000, latitude: 35.68, longitude: 139.76, spread: 0.6) + let index = MarkerGridIndex { markers } + + // A box inside one cell, one spanning several, one wider than the data, + // and one over empty ground — the last because an index that returns + // nothing is right here and wrong everywhere else. + for box in [ + bounds(35.6812, 139.7612, 35.6814, 139.7614), + bounds(35.68, 139.76, 35.69, 139.77), + bounds(35.0, 139.0, 36.5, 140.5), + bounds(10.0, 100.0, 11.0, 101.0), + ] { + let expected = ids(markers.filter { box.contains(point: $0.state.position) }) + XCTAssertEqual(ids(index.inBounds(box)), expected, "grid disagreed with a scan") + } + + // The middle two have to be carrying markers, or the comparison above + // is only proving that two empty sets match. + XCTAssertFalse(index.inBounds(bounds(35.68, 139.76, 35.69, 139.77)).isEmpty) + XCTAssertEqual(index.inBounds(bounds(35.0, 139.0, 36.5, 140.5)).count, markers.count) + } + + /// A box crossing 180° has its east corner west of its west one. + /// + /// The first version walked `lonFrom...lonTo` straight, which is an empty + /// range on Android and a trap on iOS. Fiji and the Chathams are real + /// places, and a map centred there is a reversed box every frame. + func testBoundsQueryCrossesTheAntimeridian() { + let markers = [ + entity(0, -18.0, 179.9), + entity(1, -18.0, -179.9), + entity(2, -18.0, 178.0), + entity(3, -18.0, 0.0), + ] + scatter(count: 3000, latitude: -18.0, longitude: 179.95, spread: 0.4) + + let index = MarkerGridIndex { markers } + let box = bounds(-18.5, 179.5, -17.5, -179.5) + let expected = ids(markers.filter { box.contains(point: $0.state.position) }) + + XCTAssertTrue(expected.contains("0"), "the box should hold the marker just west of 180") + XCTAssertTrue(expected.contains("1"), "and the one just east of it") + XCTAssertEqual(ids(index.inBounds(box)), expected) + } + + func testNearestMatchesBruteForce() { + let markers = scatter(count: 5000, latitude: 35.68, longitude: 139.76, spread: 0.6) + let index = MarkerGridIndex { markers } + + for point in [ + GeoPoint(latitude: 35.68, longitude: 139.76), + GeoPoint(latitude: 35.4, longitude: 139.5), + GeoPoint(latitude: 36.0, longitude: 140.1), + ] { + let expected = markers.min { + squared($0, point) < squared($1, point) + } + XCTAssertEqual(index.nearest(position: point)?.state.id, expected?.state.id) + } + } + + /// Beyond the rings the index searches it falls back to a scan rather than + /// returning nothing — the answer still has to be right, just slower. + func testNearestFallsBackWhenNothingIsClose() { + let markers = [entity(0, 35.0, 139.0)] + let index = MarkerGridIndex { markers } + XCTAssertEqual( + index.nearest(position: GeoPoint(latitude: -35.0, longitude: -70.0))?.state.id, + "0" + ) + } + + func testEmptyIndexHasNoNearest() { + let index = MarkerGridIndex { [] } + XCTAssertNil(index.nearest(position: GeoPoint(latitude: 35.0, longitude: 139.0))) + } + + /// The index borrows the manager's markers, so a change has to be seen. + func testInvalidateShowsLaterMarkers() { + var markers = [entity(0, 35.0, 139.0)] + let index = MarkerGridIndex { markers } + let box = bounds(34.9, 138.9, 35.1, 139.1) + XCTAssertEqual(index.inBounds(box).count, 1) + + markers.append(entity(1, 35.001, 139.001)) + XCTAssertEqual(index.inBounds(box).count, 1, "a stale index should stay stale until told") + + index.invalidate() + XCTAssertEqual(index.inBounds(box).count, 2) + } + + private func squared(_ entity: MarkerEntity, _ point: GeoPoint) -> Double { + let deltaLat = entity.state.position.latitude - point.latitude + let deltaLon = entity.state.position.longitude - point.longitude + return deltaLat * deltaLat + deltaLon * deltaLon + } + + /// 間引きクエリは、覆ったセルすべてから 1 本ずつ返す。 + /// + /// 呼び出し側は自分でも 1 セル 1 本に落とすつもりでこれを呼ぶ。返ってきては + /// 困るのは**穴**で、箱の中にマーカーを持つセルが何も返さないと、地図上では + /// 街路樹の無い一角になり、どこにもエラーは出ない。 + /// + /// 箱の外のマーカーが混じるのは正しい。セルの代表は箱と無関係に決まるので、 + /// 縁のセルの代表が箱の外に落ちることがある。そこを縁で切り捨てないことが、 + /// 隣のタイルと判断を揃える条件そのもの(`MarkerTileSeamTests`)。 + /// + /// android-sdk の `thinnedQueryKeepsOneMarkerFromEveryCellItCovers` と同じ。 + func testThinnedQueryKeepsOneMarkerFromEveryCellItCovers() { + let markers = scatter(count: 60_000, latitude: 35.68, longitude: 139.76, spread: 0.4) + let index = MarkerGridIndex { markers } + let box = bounds(35.60, 139.68, 35.76, 139.84) + + // index が選ぶ段を、こちらでも同じ式で出す。0.005 度に対しては段 16 -- + // 偶数段なので geocell でちょうど 9 文字になり、文字列で名指しできる。 + XCTAssertEqual(MarkerGrid.level(forSeparation: 0.005), 16) + func cellOf(_ entity: MarkerEntity) -> String { + MarkerGrid.geocell( + latitude: entity.state.position.latitude, + longitude: entity.state.position.longitude, + characters: 9 + ) + } + + guard let kept = index.inBoundsThinned(box, minSeparationDegrees: 0.005) else { + return XCTFail("0.005 度はセルより粗いので、必ず答えられるはず") + } + + let inside = markers.filter { box.contains(point: $0.state.position) } + XCTAssertGreaterThan(inside.count, 1_000, "比較できるだけの中身が要る") + + // 箱の中にいるマーカーのセルは、1 つ残らず代表を持っていること。 + XCTAssertTrue( + Set(inside.map(cellOf)).isSubset(of: Set(kept.map(cellOf))), + "埋まっているセルのどれかが、代表を返していない" + ) + XCTAssertEqual(Set(kept.map(cellOf)).count, kept.count, "同じセルから 2 本返っている") + // はみ出しは縁のセル 1 つぶんまで。箱は 0.16 度四方で段 16 のセルを + // 縦 58 x 横 30 ほど覆うから、縁は全体の 1 割に満たない。 + let outside = kept.filter { !box.contains(point: $0.state.position) } + XCTAssertLessThan( + Double(outside.count), Double(kept.count) * 0.15, + "縁のセル 1 つぶんでは説明のつかない数が箱の外にいる" + ) + // 箱は一辺 0.16 度で段 16 の正方セル(0.00275 度)を約 3,400 個覆い、 + // その中にこのマーカーが 9,600 本ほど入る -- 1 セルあたり 2.8 本。 + // これが無いと、間引く必要のないほど疎なデータでも上の assert が + // 通ってしまい、何も証明しない。 + XCTAssertLessThan(kept.count * 2, inside.count, "間引きを働かせるには疎すぎる") + } + + /// セルが呼び出し側の分離距離より粗いと、頼まれた以上に間引いてしまう。 + /// + /// 平らなグリッドだったころ、この下限は 0.005 度だった。階層になった今は + /// 一番細かい段(根から 18 段、一辺 0.000687 度)が下限で、そこまでは + /// 段を選び直して応じる。ズームが深いところで間引きが効くようになった + /// のはこの差による。 + func testThinnedQueryDeclinesWhenItsCellsAreTooCoarse() { + let index = MarkerGridIndex { + self.scatter(count: 2_000, latitude: 35.68, longitude: 139.76, spread: 0.4) + } + let box = bounds(35.6, 139.7, 35.7, 139.8) + XCTAssertNil(index.inBoundsThinned(box, minSeparationDegrees: 0.0005)) + XCTAssertNotNil( + index.inBoundsThinned(box, minSeparationDegrees: 0.001), + "0.001 度は最下段より粗いので、段を選んで応じられるはず" + ) + } + + /// 通常クエリが覚えた日付変更線の折り返しは、ここでも成り立つ必要がある。 + func testThinnedQueryCrossesTheAntimeridian() { + let markers = [ + entity(0, -18.0, 179.99), + entity(1, -18.0, -179.99), + entity(2, -18.0, 178.0), + ] + let index = MarkerGridIndex { markers } + guard let kept = index.inBoundsThinned(bounds(-18.5, 179.5, -17.5, -179.5), minSeparationDegrees: 0.01) else { + return XCTFail("答えられるはず") + } + XCTAssertEqual(ids(kept), ["0", "1"]) + } + + /// 段を自分で選ぶようになった以上、どの形の箱でも走査と一致する必要がある。 + /// + /// 四角い箱しか試していないと、新しい段選びの**軸ごとの上限**が効く経路 + /// ---極端に平たい帯、極端に細い縦帯---を一度も通らない。そこを外しても + /// 地図には何も起きず、その形の問い合わせだけが静かにマーカーを落とす。 + /// + /// 全球に近い箱をここに入れてあるのは、実際にそれで落としたからで、 + /// `MarkerGrid.columnWalk` のコメントがその中身。 + /// + /// android-sdk の `boundsQueryMatchesBruteForceForEveryShapeOfBox`、 + /// react-sdk の同名テストと対になる。 + func testBoundsQueryMatchesBruteForceForEveryShapeOfBox() { + // 2 つ目の群の id をずらす。`scatter` は毎回 0 から振るので、そのまま + // 足すと id が重なり、`ids()` が別のマーカーを 1 つに畳んでしまう -- + // 索引が重複を返していても集合としては一致し、テストが通ってしまう。 + let near = scatter(count: 30_000, latitude: 35.68, longitude: 139.76, spread: 0.8) + let far = scatter(count: 10_000, latitude: -18.0, longitude: 179.95, spread: 0.6) + .map { + entity( + Int($0.state.id)! + 30_000, + $0.state.position.latitude, + $0.state.position.longitude + ) + } + let markers = near + far + let index = MarkerGridIndex { markers } + + var nonEmpty = 0 + for (at, box) in [ + // タイル相当(z=14 から z=9 まで)。 + bounds(35.6800, 139.7600, 35.6946, 139.7820), + bounds(35.6000, 139.6000, 35.7000, 139.8000), + bounds(35.2000, 139.2000, 36.2000, 140.2000), + // 極端に平たい帯と、極端に細い縦帯。面積では段が決まらない。 + bounds(35.6790, 139.0000, 35.6810, 140.5000), + bounds(35.0000, 139.7590, 36.4000, 139.7610), + // ほぼ全球。ここが 1 列しか歩かれていなかった。 + bounds(-85.0, -179.9, 85.0, 179.9), + // 日付変更線をまたぐ箱。 + bounds(-18.5, 179.5, -17.5, -179.5), + // 何も無い場所。空を返すのが正しい唯一の箱。 + bounds(10.0, 100.0, 11.0, 101.0), + ].enumerated() { + let expected = ids(markers.filter { box.contains(point: $0.state.position) }) + let found = index.inBounds(box) + XCTAssertEqual(ids(found), expected, "箱 \(at): grid disagreed with a scan") + XCTAssertEqual(found.count, expected.count, "箱 \(at): 同じマーカーを 2 度返した") + if !expected.isEmpty { nonEmpty += 1 } + } + XCTAssertEqual(nonEmpty, 7, "空の集合どうしを比べているだけになっている") + } +} diff --git a/Tests/MapConductorCoreTests/MarkerTileCostTests.swift b/Tests/MapConductorCoreTests/MarkerTileCostTests.swift new file mode 100644 index 0000000..690032c --- /dev/null +++ b/Tests/MapConductorCoreTests/MarkerTileCostTests.swift @@ -0,0 +1,132 @@ +import XCTest + +@testable import MapConductorCore + +/// マーカータイル 1 枚の実測コスト。 +/// +/// android-vectortile の `MarkerTileCostTest` と**同じ条件**で回す。同じ LCG +/// シード、同じ緯度経度の分布、同じマーカー数、同じタイルサイズ、同じズーム。 +/// 乱数まで揃えてあるので、両者の数字は直接比べられる。 +/// +/// 片方が遅いという話は、実装のどこが遅いか分かるまで意味を持たない。 +/// レンダリングをさらに query / prepare / draw に割る `tileCostBreakdown` を +/// 用意してあるのはそのため。 +final class MarkerTileCostTests: XCTestCase { + + private let tileSize = 512 + + /// android 側と同じ線形合同法。定数まで同じなので、同じ順で同じ座標が出る。 + private struct Lcg { + private var seed: UInt64 = 12345 + mutating func next() -> Double { + seed = seed &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407 + return Double(seed >> 11) / Double(UInt64(1) << 53) + } + } + + private func manager(_ count: Int) -> MarkerManager { + let manager = MarkerManager.defaultManager(minMarkerCount: 1) + var lcg = Lcg() + for _ in 0.., + declutterPx: Int = 0 + ) -> MarkerTileRenderer { + MarkerTileRenderer( + markerManager: manager, + tileSize: tileSize, + cacheSizeBytes: 8 * 1024 * 1024, + declutterPx: declutterPx + ) + } + + private func median(_ values: [Double]) -> Double { + values.sorted()[values.count / 2] + } + + /// タイルの中心が東京になるズーム別の x/y。android 側と同じ式。 + private func tileXY(zoom: Int) -> (Int, Int) { + let n = Double(1 << zoom) + let lat = 35.68 * .pi / 180 + let x = Int((139.75 + 180.0) / 360.0 * n) + let y = Int((1.0 - log(tan(lat) + 1.0 / cos(lat)) / .pi) / 2.0 * n) + return (x, y) + } + + func testTileCost() throws { + for count in [2_000, 20_000] { + let live = manager(count) + for declutter in [0, 14] { + let render = renderer(live, declutterPx: declutter) + for zoom in [6, 12] { + let (x, y) = tileXY(zoom: zoom) + let request = TileRequest(x: x, y: y, z: zoom) + + // 1 枚目はキャッシュも何も温まっていない。中央値を採る。 + var samples: [Double] = [] + var bytes = 0 + for _ in 0..<5 { + render.clear() + let started = DispatchTime.now().uptimeNanoseconds + let data = render.renderTile(request: request) + samples.append(Double(DispatchTime.now().uptimeNanoseconds - started) / 1_000_000) + bytes = data?.count ?? 0 + } + print( + String( + format: "MARKERTILE n=%d declutter=%d z=%d render=%.1fms png=%dB", + count, declutter, zoom, median(samples), bytes + ) + ) + } + } + } + } + + /// 街路樹サンプルと同じ規模。android-vectortile の `StreetTreeCostTest` が + /// 東京の 144,183 本で測っているズームに合わせてある。 + /// + /// 数字そのものはハードウェアが違うので android と直接は比べられないが、 + /// **declutter を入れて速くなるか遅くなるか**は比べられる。android は + /// z=9 で 2140ms -> 109ms になる。 + func testStreetTreeScale() throws { + let live = manager(144_183) + for declutter in [0, 14] { + for zoom in [9, 11, 12, 14] { + let (x, y) = tileXY(zoom: zoom) + let request = TileRequest(x: x, y: y, z: zoom) + let render = renderer(live, declutterPx: declutter) + var samples: [Double] = [] + var bytes = 0 + for _ in 0..<3 { + render.clear() + let started = DispatchTime.now().uptimeNanoseconds + let data = render.renderTile(request: request) + samples.append(Double(DispatchTime.now().uptimeNanoseconds - started) / 1_000_000) + bytes = data?.count ?? 0 + } + print( + String( + format: "TREES declutter=%d z=%d render=%.0fms png=%dKB", + declutter, zoom, median(samples), bytes / 1024 + ) + ) + } + } + } +} diff --git a/Tests/MapConductorCoreTests/MarkerTileSeamTests.swift b/Tests/MapConductorCoreTests/MarkerTileSeamTests.swift new file mode 100644 index 0000000..ba1f3bb --- /dev/null +++ b/Tests/MapConductorCoreTests/MarkerTileSeamTests.swift @@ -0,0 +1,376 @@ +import UIKit +import XCTest + +@testable import MapConductorCore + +/// 隣り合うタイルは、継ぎ目にかかるマーカーについて同じ判断をしなければならない。 +/// +/// タイルは 1 枚ずつ独立に描かれる。あるマーカーがタイル A では描かれ、隣の B では +/// 描かれないと、境界でアイコンが**切れて**見える。A 側の半分だけが残り、続きが +/// どこにも無い。 +/// +/// レンダラは、アイコンが食い込む分だけ箱を広げて問い合わせる(`queryByHalfExtentPx`)。 +/// だから「A に返ったマーカーのうち B の広げた箱にも入るものは、B にも返る」が +/// 成り立てば、継ぎ目は合う。ここが見ているのはその 1 点だけで、描画には触れない。 +/// +/// android-sdk の `MarkerTileSeamTest`、react-sdk の `MarkerTileSeam.test.mjs` と +/// 同じことを見ている。 +final class MarkerTileSeamTests: XCTestCase { + + private let tileSize = 512.0 + /// サンプルと同じ declutter。継ぎ目の食い違いは間引きの副作用として出る。 + private let declutterPx = 14.0 + /// アイコン半分ぶん。レンダラは実測の最大半径を使うが、ここでは固定でよい。 + private let halfExtentPx = 10.0 + + private struct Lcg { + private var seed: UInt64 = 12345 + mutating func next() -> Double { + seed = seed &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407 + return Double(seed >> 11) / Double(UInt64(1) << 53) + } + } + + private func markers(_ count: Int) -> [MarkerEntity] { + var lcg = Lcg() + return (0.. GeoRectBounds { + func point(_ tx: Double, _ ty: Double) -> GeoPoint { + let n = pow(2.0, Double(z)) + let lon = tx / n * 360.0 - 180.0 + let lat = atan(sinh(.pi * (1.0 - 2.0 * (ty / n)))) * 180.0 / .pi + return GeoPoint(latitude: lat, longitude: lon) + } + let northWest = point(Double(x), Double(y)) + let southEast = point(Double(x + 1), Double(y + 1)) + return GeoRectBounds( + southWest: GeoPoint(latitude: southEast.latitude, longitude: northWest.longitude), + northEast: GeoPoint(latitude: northWest.latitude, longitude: southEast.longitude) + ) + } + + /// レンダラが実際に問い合わせる箱と分離距離。 + /// + /// 分離距離が最下段より細かいズームでは索引が間引きを断り、レンダラは + /// そのまま全件クエリに落ちる。そちらも継ぎ目が合っていなければならない + /// ので、断られたら `inBounds` の結果で同じことを見る。 + private func query( + _ index: MarkerGridIndex, + x: Int, + y: Int, + z: Int + ) -> (expanded: GeoRectBounds, kept: [MarkerEntity], thinned: Bool) { + let bounds = tileBounds(x: x, y: y, z: z) + let span = bounds.toSpan()! + let padNorm = halfExtentPx / tileSize + let expanded = bounds.expandedByDegrees( + latPad: span.latitude * padNorm, + lonPad: span.longitude * padNorm + ) + let separation = max(span.latitude, span.longitude) * declutterPx / tileSize + if let kept = index.inBoundsThinned(expanded, minSeparationDegrees: separation) { + return (expanded, kept, true) + } + return (expanded, index.inBounds(expanded), false) + } + + func testAdjacentTilesAgreeOnTheMarkersTheyShare() { + let all = markers(144_183) + let index = MarkerGridIndex { all } + + var checked = 0 + var thinnedPairs = 0 + for z in [9, 10, 11, 12, 13, 14] { + let n = pow(2.0, Double(z)) + let x = Int((139.75 + 180.0) / 360.0 * n) + let y = Int((1.0 - log(tan(35.68 * .pi / 180) + 1.0 / cos(35.68 * .pi / 180)) / .pi) / 2.0 * n) + + // 横隣りと縦隣り。継ぎ目は 2 方向にある。 + for (dx, dy) in [(1, 0), (0, 1)] { + let a = query(index, x: x, y: y, z: z) + let b = query(index, x: x + dx, y: y + dy, z: z) + // 低いズームではマーカーの塊が 1 枚に収まり、隣が空になる。 + // 空の側と比べても何も分からないので、その組は数えない。 + if a.kept.isEmpty || b.kept.isEmpty { continue } + checked += 1 + if a.thinned { thinnedPairs += 1 } + + let inB = Set(b.kept.map(\.state.id)) + let missing = a.kept.filter { + b.expanded.contains(point: $0.state.position) && !inB.contains($0.state.id) + } + XCTAssertEqual( + missing.count, 0, + "z=\(z) d=(\(dx),\(dy)) thinned=\(a.thinned): A が描くマーカー " + + "\(missing.count) 個を、重なる B が描かない -- 継ぎ目でアイコンが切れる" + ) + } + } + XCTAssertGreaterThanOrEqual(checked, 8, "比べられた組が少なすぎる") + XCTAssertGreaterThanOrEqual(thinnedPairs, 4, "間引きが効いている組を見ていない") + } + + // MARK: - 描いた結果で見る + + /// 描いた 2 枚の継ぎ目が、画素として破綻していないこと。 + /// + /// **粗い保険で、個々の切れは見えない。** 上のテストが数えている取りこぼしを + /// これで再現しようとしたが、街路樹の密度だと継ぎ目の列が絵で埋まり + /// (`touching` が 512 行中 512)、1 個のアイコンが欠けても画素には出ない。 + /// 索引の修正を戻して測っても数字は動かなかった。だから**通ったことを根拠に + /// してはいけない。** 継ぎ目の保証は + /// `testAdjacentTilesAgreeOnTheMarkersTheyShare` が持っている。 + /// + /// ここに残す理由は、タイルが丸ごとずれる・半分描かれないといった大きな崩れを + /// 拾うため。`touching` の assert は、継ぎ目に絵が無いのに通ってしまう + /// 空振りを防ぐためにある。 + func testRenderedTilesDoNotCutIconsAtTheSeam() throws { + let manager = MarkerManager.defaultManager(minMarkerCount: 1) + for entity in markers(40_000) { + manager.registerEntity( + MarkerEntity( + marker: nil, + state: entity.state, + visible: true, + isRendered: true, + tiling: true + ) + ) + } + let renderer = MarkerTileRenderer( + markerManager: manager, + tileSize: Int(tileSize), + cacheSizeBytes: 8 * 1024 * 1024, + declutterPx: Int(declutterPx) + ) + + var total = 0 + var seams = 0 + for z in [11, 12, 13] { + let n = pow(2.0, Double(z)) + let x = Int((139.75 + 180.0) / 360.0 * n) + let y = Int((1.0 - log(tan(35.68 * .pi / 180) + 1.0 / cos(35.68 * .pi / 180)) / .pi) / 2.0 * n) + + guard let left = renderer.renderTile(request: TileRequest(x: x, y: y, z: z)), + let right = renderer.renderTile(request: TileRequest(x: x + 1, y: y, z: z)), + let leftAlpha = Self.alphaRows(left, column: Int(tileSize) - 1), + let rightAlpha = Self.alphaRows(right, column: 0) + else { + return XCTFail("z=\(z) のタイルが描けない") + } + seams += 1 + + var cut = 0 + var touching = 0 + for row in 0.. 0 { touching += 1 } + if leftAlpha[row] > 0 && rightAlpha[row] == 0 { cut += 1 } + } + // 左タイルの最終列に何も無ければ、この比較は空振り。 + XCTAssertGreaterThan(touching, 0, "z=\(z) の継ぎ目に絵が無く、何も検証していない") + print("SEAM z=\(z) cut=\(cut) / touching=\(touching) / \(leftAlpha.count) rows") + total += cut + } + XCTAssertEqual(seams, 3, "3 枚とも描けていないと比較にならない") + XCTAssertLessThan(total, 20, "継ぎ目で途切れている行が多すぎる -- アイコンが切れている") + } + + /// PNG の 1 列ぶんのアルファ。 + private static func alphaRows(_ png: Data, column: Int) -> [UInt8]? { + guard let image = UIImage(data: png)?.cgImage else { return nil } + let width = image.width + let height = image.height + var pixels = [UInt8](repeating: 0, count: width * height * 4) + guard let context = CGContext( + data: &pixels, width: width, height: height, + bitsPerComponent: 8, bytesPerRow: width * 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return nil } + context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height)) + return (0.. ImageIcon { + let size = CGSize(width: 20, height: 20) + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { context in + color.setFill() + context.fill(CGRect(origin: .zero, size: size)) + } + return ImageIcon(image: image, iconSize: 20) + } + let manager = MarkerManager.defaultManager(minMarkerCount: 1) + for (at, offsetPx) in [3.0, 6.0].enumerated() { + manager.registerEntity( + MarkerEntity( + marker: nil, + state: MarkerState( + position: GeoPoint(latitude: centerLat, longitude: boundaryLon - offsetPx * lonPerPx), + id: "seam-\(at)", + icon: solid(at == 0 ? .red : .blue) + ), + visible: true, + isRendered: true, + tiling: true + ) + ) + } + let renderer = MarkerTileRenderer( + markerManager: manager, + tileSize: 512, + cacheSizeBytes: 1024 * 1024, + declutterPx: 14 + ) + + func colors(_ png: Data) throws -> (red: Int, blue: Int) { + let image = try XCTUnwrap(UIImage(data: png)?.cgImage) + var pixels = [UInt8](repeating: 0, count: image.width * image.height * 4) + let context = CGContext( + data: &pixels, width: image.width, height: image.height, + bitsPerComponent: 8, bytesPerRow: image.width * 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + context?.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) + var red = 0, blue = 0 + for at in stride(from: 0, to: pixels.count, by: 4) where pixels[at + 3] > 200 { + if pixels[at] > 200 && pixels[at + 2] < 60 { red += 1 } + if pixels[at + 2] > 200 && pixels[at] < 60 { blue += 1 } + } + return (red, blue) + } + + let west = try XCTUnwrap(renderer.renderTile(request: TileRequest(x: boundaryX - 1, y: y, z: z))) + let east = try XCTUnwrap(renderer.renderTile(request: TileRequest(x: boundaryX, y: y, z: z))) + let w = try colors(west) + let e = try colors(east) + + // 2 本は同じセルなので、生き残りは 1 本だけ。 + XCTAssertTrue((w.red == 0) != (w.blue == 0), "西タイルに両方(または どちらも)描かれている: \(w)") + // マーカーは境界の 3px 西なので、20px のアイコンは東タイルへ 7px はみ出す。 + // 東タイルにも**同じ色**が出ていなければ、境界で切れている。 + XCTAssertTrue((e.red > 0) || (e.blue > 0), "東タイルにはみ出し分が描かれていない") + XCTAssertEqual(w.red > 0, e.red > 0, "左右のタイルが別の木を勝者に選んだ") + XCTAssertEqual(w.blue > 0, e.blue > 0, "左右のタイルが別の木を勝者に選んだ") + } + + /// 同じセルの勝者は、マーカーの**登録順にも**依存しない。 + /// + /// 間引きの勝者を「列挙順で最後」にしていたころ、同セル内の列挙順は + /// `allEntities()` の辞書順 -- 索引を作り直すたびに変わり得る値だった。 + /// 隣のタイルは別の時刻(別の再構築世代)に描かれるので、同じセルを + /// 共有する 2 枚が別の木を勝者に選び、上半分と下半分が別の色の + /// 「割れた丸」になる -- 御茶ノ水の実機で出た形。ここでは同一座標・ + /// 異色の 2 本を**登録順を逆にした 2 つのマネージャ**で描き、どちらも + /// 同じ色を勝者に選ぶことを確かめる。 + func testDeclutterWinnerDoesNotDependOnRegistrationOrder() throws { + let z = 16 + let n = pow(2.0, Double(z)) + let x = Int((139.765 + 180.0) / 360.0 * n) + let y = Int((1.0 - log(tan(35.68 * .pi / 180) + 1.0 / cos(35.68 * .pi / 180)) / .pi) / 2.0 * n) + let lat = atan(sinh(.pi * (1.0 - 2.0 * (Double(y) + 0.5) / n))) * 180.0 / .pi + let lon = (Double(x) + 0.5) / n * 360.0 - 180.0 + + func solid(_ color: UIColor) -> ImageIcon { + let size = CGSize(width: 20, height: 20) + return ImageIcon( + image: UIGraphicsImageRenderer(size: size).image { context in + color.setFill() + context.fill(CGRect(origin: .zero, size: size)) + }, + iconSize: 20 + ) + } + // 同じ ImageIcon インスタンスを両方のマネージャで使う。実機でも種ごとの + // アイコンは共有されるので、これが実際の形。 + let red = solid(.red) + let blue = solid(.blue) + + func png(order: [(String, ImageIcon)]) throws -> Data { + let manager = MarkerManager.defaultManager(minMarkerCount: 1) + for (id, icon) in order { + manager.registerEntity( + MarkerEntity( + marker: nil, + state: MarkerState( + position: GeoPoint(latitude: lat, longitude: lon), + id: id, + icon: icon + ), + visible: true, + isRendered: true, + tiling: true + ) + ) + } + let renderer = MarkerTileRenderer( + markerManager: manager, + tileSize: 512, + cacheSizeBytes: 1024 * 1024, + declutterPx: 14 + ) + return try XCTUnwrap(renderer.renderTile(request: TileRequest(x: x, y: y, z: z))) + } + func colors(_ png: Data) throws -> (red: Int, blue: Int) { + let image = try XCTUnwrap(UIImage(data: png)?.cgImage) + var pixels = [UInt8](repeating: 0, count: image.width * image.height * 4) + let context = CGContext( + data: &pixels, width: image.width, height: image.height, + bitsPerComponent: 8, bytesPerRow: image.width * 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + context?.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) + var red = 0, blue = 0 + for at in stride(from: 0, to: pixels.count, by: 4) where pixels[at + 3] > 200 { + if pixels[at] > 200 && pixels[at + 2] < 60 { red += 1 } + if pixels[at + 2] > 200 && pixels[at] < 60 { blue += 1 } + } + return (red, blue) + } + + let forward = try colors(try png(order: [("a", red), ("b", blue)])) + let reversed = try colors(try png(order: [("b", blue), ("a", red)])) + print("ORDERSEAM forward=\(forward) reversed=\(reversed)") + XCTAssertTrue((forward.red == 0) != (forward.blue == 0), "1 本に間引かれていない: \(forward)") + XCTAssertEqual(forward.red > 0, reversed.red > 0, "登録順で勝者が変わった: \(forward) vs \(reversed)") + XCTAssertEqual(forward.blue > 0, reversed.blue > 0, "登録順で勝者が変わった: \(forward) vs \(reversed)") + } +} diff --git a/Tests/MapConductorCoreTests/MarkerTilingFlagTests.swift b/Tests/MapConductorCoreTests/MarkerTilingFlagTests.swift index ae798dd..b6aec51 100644 --- a/Tests/MapConductorCoreTests/MarkerTilingFlagTests.swift +++ b/Tests/MapConductorCoreTests/MarkerTilingFlagTests.swift @@ -131,8 +131,15 @@ final class MarkerTilingFlagTests: XCTestCase { // MARK: - Tile renderer - /// `MarkerTileRenderer` はタイル担当だけを焼く。ネイティブ担当しか無ければタイルは出ない。 - func testTileRendererSkipsNativeEntities() { + /// `MarkerTileRenderer` はタイル担当だけを焼く。ネイティブ担当しか無ければ + /// 描くものが無いので nil を返す。 + /// + /// nil は「タイルが無い」ではなく「ここは空」。透明な絵に変えるのは + /// `LocalTileServer` の仕事で、そこが 404 や 503 を返さないことは + /// `LocalTileServerSemanticsTests` が見ている。一時期ここで透明タイルを + /// 作っていたが、同じく空で nil を返す geojson / kml / groundimage / + /// vectortile だけが 503 になるので、android-sdk と同じくサーバ一点へ寄せた。 + func testTileRendererSkipsNativeEntities() throws { let manager = MarkerManager() manager.registerEntity( MarkerEntity( @@ -145,7 +152,10 @@ final class MarkerTilingFlagTests: XCTestCase { ) let renderer = MarkerTileRenderer(markerManager: manager) - XCTAssertNil(renderer.renderTile(request: Self.tileRequest)) + XCTAssertNil( + renderer.renderTile(request: Self.tileRequest), + "ネイティブ担当のマーカーがタイルに描かれている" + ) } /// タイル担当が居ればタイルは出る(上のテストが「常に nil」で通らないことの確認)。 diff --git a/Tests/MapConductorCoreTests/MetalMarkerRasterizerTests.swift b/Tests/MapConductorCoreTests/MetalMarkerRasterizerTests.swift new file mode 100644 index 0000000..5754277 --- /dev/null +++ b/Tests/MapConductorCoreTests/MetalMarkerRasterizerTests.swift @@ -0,0 +1,173 @@ +import UIKit +import XCTest + +@testable import MapConductorCore + +/// `MetalMarkerRasterizer` を renderer から切り離して単体で責める。 +/// +/// 実機で「タイル右端のマーカーが消える」「別のアイコンの色が出る」が +/// 出た(`DeviceSeamReproBench`)。renderer 側の kept・順序・矩形は左右の +/// タイルで一致していたので、容疑はこの中 -- アトラスの詰め方、UV、 +/// クアッドの位置、読み戻しのどれか。ここでは入力を手で組むので、 +/// どの段が嘘をつくかが直接分かる。 +final class MetalMarkerRasterizerTests: XCTestCase { + + private func solid(_ color: UIColor, size: Int = 16) -> CGImage { + let renderer = UIGraphicsImageRenderer(size: CGSize(width: size, height: size)) + return renderer.image { context in + color.setFill() + context.fill(CGRect(x: 0, y: 0, width: size, height: size)) + }.cgImage! + } + + /// 上半分と下半分で色の違う 1 枚。アトラスの上下反転はこれで出る。 + private func split(top: UIColor, bottom: UIColor, size: Int = 16) -> CGImage { + let renderer = UIGraphicsImageRenderer(size: CGSize(width: size, height: size)) + return renderer.image { context in + top.setFill() + context.fill(CGRect(x: 0, y: 0, width: size, height: size / 2)) + bottom.setFill() + context.fill(CGRect(x: 0, y: size / 2, width: size, height: size / 2)) + }.cgImage! + } + + private func pixels(_ png: Data) throws -> (p: [UInt8], w: Int, h: Int) { + let image = try XCTUnwrap(UIImage(data: png)?.cgImage) + var buffer = [UInt8](repeating: 0, count: image.width * image.height * 4) + let context = CGContext( + data: &buffer, width: image.width, height: image.height, + bitsPerComponent: 8, bytesPerRow: image.width * 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + context?.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) + return (buffer, image.width, image.height) + } + + private func color(_ data: (p: [UInt8], w: Int, h: Int), _ x: Int, _ y: Int) -> (Int, Int, Int, Int) { + let at = (y * data.w + x) * 4 + return (Int(data.p[at]), Int(data.p[at + 1]), Int(data.p[at + 2]), Int(data.p[at + 3])) + } + + /// 2x2 アトラスの 4 色が、それぞれのインスタンスにそのまま出ること。 + /// アトラスの行が入れ替わっていれば(上下反転)、ここで色が入れ替わる。 + func testFourIconsKeepTheirColours() throws { + let gpu = try XCTUnwrap(MetalMarkerRasterizer.createOrNull(tileSize: 128)) + let icons = [solid(.red), solid(.green), solid(.blue), solid(.yellow)] + let atlas = try XCTUnwrap(gpu.makeAtlas(icons)) + var instances: [MetalMarkerRasterizer.Instance] = [] + for at in 0..<4 { + let uv = atlas.uv[at] + instances.append(MetalMarkerRasterizer.Instance( + left: Float(8 + at * 30), top: 40, width: 16, height: 16, + u0: Float(uv.minX), v0: Float(uv.minY), + u1: Float(uv.maxX), v1: Float(uv.maxY) + )) + } + let png = try XCTUnwrap(gpu.renderPng(instances: instances, atlas: atlas.texture, paddingPx: 0)) + let out = try pixels(png) + let expected = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)] + for at in 0..<4 { + let (r, g, b, a) = color(out, 8 + at * 30 + 8, 48) + XCTAssertGreaterThan(a, 200, "インスタンス \(at) が描かれていない") + let want = expected[at] + XCTAssertTrue( + abs(r - want.0) < 40 && abs(g - want.1) < 40 && abs(b - want.2) < 40, + "インスタンス \(at): 期待 \(want) 実際 (\(r),\(g),\(b)) -- アトラスの取り違え" + ) + } + } + + /// アイコンの上下がそのまま出ること(上下反転の検出)。 + func testIconIsNotUpsideDown() throws { + let gpu = try XCTUnwrap(MetalMarkerRasterizer.createOrNull(tileSize: 64)) + let icons = [split(top: .red, bottom: .blue)] + let atlas = try XCTUnwrap(gpu.makeAtlas(icons)) + let uv = atlas.uv[0] + let instance = MetalMarkerRasterizer.Instance( + left: 8, top: 8, width: 16, height: 16, + u0: Float(uv.minX), v0: Float(uv.minY), u1: Float(uv.maxX), v1: Float(uv.maxY) + ) + let png = try XCTUnwrap(gpu.renderPng(instances: [instance], atlas: atlas.texture, paddingPx: 0)) + let out = try pixels(png) + let top = color(out, 16, 11) + let bottom = color(out, 16, 21) + XCTAssertGreaterThan(top.0, 180, "上半分が赤でない: \(top)") + XCTAssertGreaterThan(bottom.2, 180, "下半分が青でない: \(bottom)") + } + + /// 多行アトラス(9 枚 → 3x3)でも取り違えないこと。 + func testNineIconsKeepTheirColoursAcrossRows() throws { + let gpu = try XCTUnwrap(MetalMarkerRasterizer.createOrNull(tileSize: 256)) + let hues: [CGFloat] = [0.0, 0.1, 0.2, 0.35, 0.5, 0.6, 0.7, 0.8, 0.9] + let icons = hues.map { solid(UIColor(hue: $0, saturation: 1, brightness: 1, alpha: 1)) } + let atlas = try XCTUnwrap(gpu.makeAtlas(icons)) + var instances: [MetalMarkerRasterizer.Instance] = [] + for at in 0..<9 { + let uv = atlas.uv[at] + instances.append(MetalMarkerRasterizer.Instance( + left: Float(4 + (at % 5) * 40), top: Float(20 + (at / 5) * 60), + width: 16, height: 16, + u0: Float(uv.minX), v0: Float(uv.minY), + u1: Float(uv.maxX), v1: Float(uv.maxY) + )) + } + let png = try XCTUnwrap(gpu.renderPng(instances: instances, atlas: atlas.texture, paddingPx: 0)) + let out = try pixels(png) + for at in 0..<9 { + var want = (r: CGFloat(0), g: CGFloat(0), b: CGFloat(0), a: CGFloat(0)) + UIColor(hue: hues[at], saturation: 1, brightness: 1, alpha: 1) + .getRed(&want.r, green: &want.g, blue: &want.b, alpha: &want.a) + let (r, g, b, a) = color(out, 4 + (at % 5) * 40 + 8, 20 + (at / 5) * 60 + 8) + XCTAssertGreaterThan(a, 200, "インスタンス \(at) が描かれていない") + XCTAssertTrue( + abs(r - Int(want.r * 255)) < 50 && abs(g - Int(want.g * 255)) < 50 && abs(b - Int(want.b * 255)) < 50, + "インスタンス \(at): 期待 (\(Int(want.r * 255)),\(Int(want.g * 255)),\(Int(want.b * 255))) 実際 (\(r),\(g),\(b))" + ) + } + } + + /// 右端で一部だけはみ出すインスタンスは、はみ出さない部分が描かれること。 + /// 実機では右端のマーカーが丸ごと消えた。 + func testInstanceOverhangingTheRightEdgeStillDraws() throws { + let size = 128 + let gpu = try XCTUnwrap(MetalMarkerRasterizer.createOrNull(tileSize: size)) + let icons = [solid(.red)] + let atlas = try XCTUnwrap(gpu.makeAtlas(icons)) + let uv = atlas.uv[0] + // 中心がタイル右端ちょうど: 左半分だけ見えるはず。 + let instance = MetalMarkerRasterizer.Instance( + left: Float(size - 8), top: 40, width: 16, height: 16, + u0: Float(uv.minX), v0: Float(uv.minY), u1: Float(uv.maxX), v1: Float(uv.maxY) + ) + let png = try XCTUnwrap(gpu.renderPng(instances: [instance], atlas: atlas.texture, paddingPx: 0)) + let out = try pixels(png) + let inside = color(out, size - 4, 48) + XCTAssertGreaterThan(inside.3, 200, "右端にかかるインスタンスが消えた: \(inside)") + XCTAssertGreaterThan(inside.0, 180, "色が違う: \(inside)") + } + + /// 同じ入力から 2 回描いて同じ画素が出ること。実機の再現は走行ごとに + /// 割れる境界が揺れた -- 非決定性はそれ自体が欠陥。 + func testSameInputsSamePixels() throws { + let gpu = try XCTUnwrap(MetalMarkerRasterizer.createOrNull(tileSize: 256)) + let icons = (0..<7).map { solid(UIColor(hue: CGFloat($0) / 7.0, saturation: 1, brightness: 1, alpha: 1)) } + let atlas = try XCTUnwrap(gpu.makeAtlas(icons)) + var instances: [MetalMarkerRasterizer.Instance] = [] + var seed = UInt64(9) + func next() -> Float { + seed = seed &* 6_364_136_223_846_793_005 &+ 1 + return Float(seed >> 40) / Float(1 << 24) + } + for at in 0..<200 { + let uv = atlas.uv[at % icons.count] + instances.append(MetalMarkerRasterizer.Instance( + left: next() * 260 - 10, top: next() * 260 - 10, width: 16, height: 16, + u0: Float(uv.minX), v0: Float(uv.minY), u1: Float(uv.maxX), v1: Float(uv.maxY) + )) + } + let first = try XCTUnwrap(gpu.renderPng(instances: instances, atlas: atlas.texture, paddingPx: 0)) + let second = try XCTUnwrap(gpu.renderPng(instances: instances, atlas: atlas.texture, paddingPx: 0)) + XCTAssertEqual(first, second, "同じ入力で違う絵が出た") + } +} diff --git a/Tests/MapConductorCoreTests/OverlayCollectorDebounceTests.swift b/Tests/MapConductorCoreTests/OverlayCollectorDebounceTests.swift index ce5add2..b2be165 100644 --- a/Tests/MapConductorCoreTests/OverlayCollectorDebounceTests.swift +++ b/Tests/MapConductorCoreTests/OverlayCollectorDebounceTests.swift @@ -14,19 +14,15 @@ import XCTest @MainActor final class OverlayCollectorDebounceTests: XCTestCase { - /// テスト用の最小 state。`overlayChangePublisher()` を手で叩ける。 + /// テスト用の最小 state。`mutate()` で書き込みが起きたことにできる。 private final class TestState: OverlayCollectableState { let id: String - private let subject = PassthroughSubject() + let mutations = StateMutationSignal() init(id: String) { self.id = id } - func overlayChangePublisher() -> AnyPublisher { - subject.eraseToAnyPublisher() - } - /// in-place 変更が起きたことにする。 - func mutate() { subject.send(()) } + func mutate() { mutations.notifyMutated() } } /// 窓(5ms)より十分長く待つ。 diff --git a/Tests/MapConductorCoreTests/OverlayCollectorScaleTests.swift b/Tests/MapConductorCoreTests/OverlayCollectorScaleTests.swift new file mode 100644 index 0000000..8fdc90b --- /dev/null +++ b/Tests/MapConductorCoreTests/OverlayCollectorScaleTests.swift @@ -0,0 +1,69 @@ +import Combine +import XCTest + +@testable import MapConductorCore + +/// What one subscription per state costs when there are a lot of states. +/// +/// android-sdk had the opposite problem: it watched every state through a +/// single snapshot observer, which cost the size of the collection on every +/// commit and made a drag with 144k markers stall for 350 ms a frame. iOS and +/// the web SDK already push — each state publishes its own changes — so that +/// cost does not exist here. What does exist is the setup: `asFlow()` builds a +/// CombineLatest of a CombineLatest4 per marker, and the collector wraps each +/// in `receive(on:)` and a sink. +/// +/// Tokyo's street trees are 144,183 markers. This measures the shape of the +/// curve rather than asserting a number, because a number would be about the +/// machine; the test fails only if it does not finish. +@MainActor +final class OverlayCollectorScaleTests: XCTestCase { + + private func markers(_ count: Int) -> [MarkerState] { + (0..() + var membership = 0 + collector.onMembershipChange { _ in membership += 1 } + collector.onStateChange { _ in } + + let started = Date() + collector.sync(states) + let elapsed = Date().timeIntervalSince(started) + + print(String(format: "IOSCOLLECTOR n=%d sync=%.0fms", count, elapsed * 1000)) + XCTAssertEqual(collector.values().count, count) + collector.clear() + } + } + + /// The per-state publisher on its own, with the collector out of the way. + func testPublisherSetupCost() { + for count in [1_000, 10_000, 50_000] { + let states = markers(count) + var bag: [AnyCancellable] = [] + bag.reserveCapacity(count) + + let started = Date() + for state in states { + bag.append(state.asFlow().sink { _ in }) + } + let elapsed = Date().timeIntervalSince(started) + + print(String(format: "IOSPUBLISHER n=%d subscribe=%.0fms", count, elapsed * 1000)) + XCTAssertEqual(bag.count, count) + } + } +} diff --git a/Tests/MapConductorCoreTests/ProjectionConformanceTests.swift b/Tests/MapConductorCoreTests/ProjectionConformanceTests.swift new file mode 100644 index 0000000..e0fe4b3 --- /dev/null +++ b/Tests/MapConductorCoreTests/ProjectionConformanceTests.swift @@ -0,0 +1,78 @@ +import XCTest + +@testable import MapConductorCore + +/// Web Mercator の適合テスト。 +/// +/// 同じ `projection-vectors.txt` を android-sdk / react-sdk も読む。3 つが同じ +/// 答えを出すことを担保するのが目的で、ここが見ているのはこのプラットフォームの +/// 分だけ。突き合わせは 3 つの出力に対して別途行う。 +/// +/// `zoom-golden.txt` とは役割が違う。あちらは移行前の値の記録(既知の不具合込み)。 +/// こちらは現在の実装どうしが一致しているかを見る。 +final class ProjectionConformanceTests: XCTestCase { + + /// 桁を固定する。 + /// + /// 言語ごとの既定の数値表記は揃わない。f64 のノイズより粗く、実装差より細かい + /// 桁で切る。 + private func fixed(_ value: Double) -> String { + if value.isNaN { return "nan" } + if value == .infinity { return "inf" } + if value == -.infinity { return "-inf" } + return String(format: "%.6f", value) + } + + private func runVectors() throws -> String { + guard let url = Bundle.module.url(forResource: "projection-vectors", withExtension: "txt") else { + XCTFail("projection-vectors.txt が見つからない") + return "" + } + let projection = WebMercatorProjection() + var out = "" + for line in try String(contentsOf: url, encoding: .utf8).split(separator: "\n", omittingEmptySubsequences: false) { + let row = line.trimmingCharacters(in: .whitespaces) + if row.isEmpty || row.hasPrefix("#") { continue } + let parts = row.split(separator: "|", omittingEmptySubsequences: false).map(String.init) + let (op, a, b) = (parts[0], parts[1], parts[2]) + switch op { + case "project": + let p = projection.project(GeoPoint(latitude: Double(a)!, longitude: Double(b)!)) + out += "project|\(a)|\(b)|\(fixed(p.x))|\(fixed(p.y))\n" + case "unproject": + let g = projection.unproject(CGPoint(x: Double(a)!, y: Double(b)!)) + out += "unproject|\(a)|\(b)|\(fixed(g.latitude))|\(fixed(g.longitude))\n" + default: + XCTFail("未知の演算: \(op)") + } + } + return out + } + + func testVectorsAllRun() throws { + let actual = try runVectors() + + // 突き合わせに使う。テストの副産物ではなく、これが成果物。 + // シミュレータのサンドボックス内なので、ログにも出して外から拾えるようにする。 + let destination = FileManager.default.temporaryDirectory + .appendingPathComponent("projection-actual.ios.txt") + try actual.write(to: destination, atomically: true, encoding: .utf8) + print("CONFORMANCE_BEGIN") + print(actual, terminator: "") + print("CONFORMANCE_END") + + let rows = actual.split(separator: "\n").map(String.init) + XCTAssertFalse(rows.isEmpty, "ベクタが 1 行も処理されていない") + for row in rows { + XCTAssertEqual(row.split(separator: "|").count, 5, "列数が合わない: \(row)") + } + } + + /// 正本が決まるまでは存在しない。3 プラットフォームの差を潰してから凍結する。 + func testMatchesCanonicalIfPresent() throws { + guard let url = Bundle.module.url(forResource: "projection-expected", withExtension: "txt") else { + throw XCTSkip("正本がまだない") + } + XCTAssertEqual(try String(contentsOf: url, encoding: .utf8), try runVectors()) + } +} diff --git a/Tests/MapConductorCoreTests/Resources/geocell-expected.txt b/Tests/MapConductorCoreTests/Resources/geocell-expected.txt new file mode 100644 index 0000000..132fe0a --- /dev/null +++ b/Tests/MapConductorCoreTests/Resources/geocell-expected.txt @@ -0,0 +1,82 @@ +geocell|0.0|-90.0|1|0 +geocell|0.0|90.0|1|1 +geocell|0.0|-0.0001|1|0 +geocell|0.0|0.0001|1|1 +geocell|0.0|0.0|2|18 +geocell|0.0001|0.0001|2|18 +geocell|-0.0001|0.0001|2|12 +geocell|0.0001|-0.0001|2|0d +geocell|-0.0001|-0.0001|2|07 +geocell|35.681236|139.767125|1|1 +geocell|35.681236|139.767125|2|1d +geocell|35.681236|139.767125|3|1da +geocell|35.681236|139.767125|4|1da1 +geocell|35.681236|139.767125|5|1da1c +geocell|35.681236|139.767125|6|1da1cd +geocell|35.681236|139.767125|7|1da1cda +geocell|35.681236|139.767125|8|1da1cdab +geocell|35.681236|139.767125|9|1da1cdabf +geocell|35.681236|139.767125|10|1da1cdabf4 +geocell|-18.0|179.9999|10|17d7d7d7d7 +geocell|-18.0|-179.9999|10|0282828282 +geocell|-18.0|180.0|10|0282828282 +geocell|-18.0|-180.0|10|0282828282 +geocell|90.0|0.0|10|1aaaaaaaaa +geocell|-90.0|0.0|10|1000000000 +geocell|89.9999|0.0|5|1aaaa +geocell|-89.9999|0.0|5|10000 +geocell|0.0|180.0|4|0800 +geocell|0.0|-180.0|4|0800 +geocell|0.0|179.9999|4|1d55 +geocell|-33.868820|151.209290|6|171bfa +geocell|51.507351|-0.127758|6|0f5d77 +geocell|-22.906847|-43.172896|6|072ae6 +geocell|64.128288|-21.827774|6|0f68a5 +level|360.0|0 +level|180.0|0 +level|1.0|8 +level|0.1|11 +level|0.01|15 +level|0.005|16 +level|0.004|16 +level|0.0007|18 +level|0.000687|18 +level|0.0005|none +level|0.0|none +level|-1.0|none +cell|0.0|0.0|0|0,1 +cell|0.0|0.0|1|1,2 +cell|-90.0|-180.0|1|0,0 +cell|90.0|180.0|1|1,0 +cell|35.681236|139.767125|18|183036,465694 +cell|-18.0|179.9999|18|104857,524287 +cell|-18.0|-179.9999|18|104857,0 +morton|0|0|1|0 +morton|1|0|1|2 +morton|0|1|1|1 +morton|1|1|1|3 +morton|0|2|1|4 +morton|1|3|1|7 +morton|3|0|2|10 +morton|0|3|2|5 +morton|3|7|2|31 +morton|262143|262143|18|68719476735 +morton|262143|524287|18|137438953471 +morton|0|524287|18|91625968981 +morton|174219|203548|18|58145997274 +qlevel|0.0146|0.0220|18 +qlevel|0.46|0.703|13 +qlevel|0.16|0.16|15 +qlevel|180.0|360.0|4 +qlevel|0.0|360.0|5 +qlevel|180.0|0.0|6 +qlevel|0.000000001|0.000000001|18 +qlevel|170.0|0.001|6 +qlevel|0.001|170.0|6 +colwalk|-180.0|180.0|4|0,32 +colwalk|-180.2|179.8|4|-1,32 +colwalk|-179.9|179.9|4|0,32 +colwalk|139.5|140.5|11|3635,12 +colwalk|179.5|-179.5|11|4090,12 +colwalk|-0.5|0.5|9|510,4 +colwalk|179.95|179.96|18|524215,15 diff --git a/Tests/MapConductorCoreTests/Resources/geocell-vectors.txt b/Tests/MapConductorCoreTests/Resources/geocell-vectors.txt new file mode 100644 index 0000000..e9d554d --- /dev/null +++ b/Tests/MapConductorCoreTests/Resources/geocell-vectors.txt @@ -0,0 +1,146 @@ +# geocell 適合ベクタ。android-sdk / ios-sdk / react-sdk で同一のファイル。 +# +# ## これは何か +# +# マーカーの空間索引が使う階層セルの名前。3 つの SDK は鍵を Long / Int64 / +# number と別々の型で持つので、数値そのものではなく**文字列で**突き合わせる。 +# +# 世界は**正方の 2 セル**(西半球と東半球、一辺 180 度)から始まり、そこから +# 2x2 に細分される。文字列は半球の数字 1 桁のあと、4x4 の細分 1 段ごとに +# 0123456789abcdef の 1 文字が付く。緯度ビットが経度ビットの上。 +# +# cordova-plugin-googlemaps の geomodel.js は根が 1 セルで、セルが縦横 2:1 に +# なる。そちらに合わせていたが、幅を分離距離に合わせると高さが 2 倍細かくなり、 +# 問い合わせが必要の 2 倍のセルを歩いて 2 倍のマーカーを返していた(東京の +# 街路樹・ズーム 11 で、返した 9,715 件のうち呼び出し側が残したのは 2,156 件)。 +# 根を正方 2 枚に割るとその 2 倍が消える。文字列で名前を付けられる性質は残る。 +# +# 一致していないと何が起きるか: どのプラットフォームでも地図は描ける。セルの +# 切れ目が違うだけなので、間引いたときに残る木が違う、という形でしか出ない。 +# 目で見て気づける差ではないから、ここで止める。 +# +# ## 書式 +# +# geocell|緯度|経度|文字数 -> 文字列(半球 1 桁 + 4x4 の文字) +# level|分離距離(度) -> 段(応じられなければ none) +# cell|緯度|経度|段 -> 緯度インデックス,経度インデックス +# morton|緯度インデックス|経度インデックス|段 -> 鍵(10 進) +# qlevel|緯度の幅|経度の幅 -> 範囲クエリが歩く段 +# colwalk|west|east|段 -> 開始列,列数 +# +# 列は | 区切り。# で始まる行と空行は読み飛ばす。 + +# 半球だけ。1 文字は西(0)か東(1)かしか言わない。 +geocell|0.0|-90.0|1 +geocell|0.0|90.0|1 +geocell|0.0|-0.0001|1 +geocell|0.0|0.0001|1 + +# 原点まわり。4 つの象限がそれぞれ別の文字になること。 +geocell|0.0|0.0|2 +geocell|0.0001|0.0001|2 +geocell|-0.0001|0.0001|2 +geocell|0.0001|-0.0001|2 +geocell|-0.0001|-0.0001|2 + +# 東京。深さを 1 つずつ増やすと、前の答えが接頭辞になっていること。 +geocell|35.681236|139.767125|1 +geocell|35.681236|139.767125|2 +geocell|35.681236|139.767125|3 +geocell|35.681236|139.767125|4 +geocell|35.681236|139.767125|5 +geocell|35.681236|139.767125|6 +geocell|35.681236|139.767125|7 +geocell|35.681236|139.767125|8 +geocell|35.681236|139.767125|9 +geocell|35.681236|139.767125|10 + +# 日付変更線の両側。179.9999 と -179.9999 は別のセルで、端の折り返しで +# 同じ名前になったりしないこと。半球の桁も分かれる。 +geocell|-18.0|179.9999|10 +geocell|-18.0|-179.9999|10 +geocell|-18.0|180.0|10 +geocell|-18.0|-180.0|10 + +# 極。90 度ちょうどが行の外へはみ出さないこと。 +geocell|90.0|0.0|10 +geocell|-90.0|0.0|10 +geocell|89.9999|0.0|5 +geocell|-89.9999|0.0|5 + +# 経度 0 と 180 の境目、緯度 0 の境目。 +geocell|0.0|180.0|4 +geocell|0.0|-180.0|4 +geocell|0.0|179.9999|4 + +# 南半球・西半球のふつうの点。 +geocell|-33.868820|151.209290|6 +geocell|51.507351|-0.127758|6 +geocell|-22.906847|-43.172896|6 +geocell|64.128288|-21.827774|6 + +# 段の選択。分離距離より粗いセルは返さない。最下段は一辺 0.000687 度。 +level|360.0 +level|180.0 +level|1.0 +level|0.1 +level|0.01 +level|0.005 +level|0.004 +level|0.0007 +level|0.000687 +level|0.0005 +level|0.0 +level|-1.0 + +# セルのインデックス。境界がどちら側に入るか。列は行の 2 倍あること。 +cell|0.0|0.0|0 +cell|0.0|0.0|1 +cell|-90.0|-180.0|1 +cell|90.0|180.0|1 +cell|35.681236|139.767125|18 +cell|-18.0|179.9999|18 +cell|-18.0|-179.9999|18 + +# 鍵の組み立て。ビットの並び順がずれていれば、ここで出る。 +# 半球のビットは組の上に乗る -- 下 2 ビットを落とせば親になること。 +morton|0|0|1 +morton|1|0|1 +morton|0|1|1 +morton|1|1|1 +morton|0|2|1 +morton|1|3|1 +morton|3|0|2 +morton|0|3|2 +morton|3|7|2 +morton|262143|262143|18 +morton|262143|524287|18 +morton|0|524287|18 +morton|174219|203548|18 + +# 範囲クエリが選ぶ段。面積で決めたあと、軸ごとの上限で戻す。 +# +# 一致していないと、3 つは同じセルを名指しながら違う粗さで歩くことになる。 +# 答えは変わらないので、テストでは出ず、速度の差としてしか出ない。 +qlevel|0.0146|0.0220 +qlevel|0.46|0.703 +qlevel|0.16|0.16 +qlevel|180.0|360.0 +qlevel|0.0|360.0 +qlevel|180.0|0.0 +qlevel|0.000000001|0.000000001 +qlevel|170.0|0.001 +qlevel|0.001|170.0 + +# 列の歩き方。開始列と、歩く列数。 +# +# 経度の列は折り返すので、東端の列から終わりを決められない(180 の列と -180 の +# 列は同じ)。west と、そこから東向きの幅から出す。ここがずれると、地図には +# 何も起きないまま、広い箱だけが世界の一部しか見なくなる。 +colwalk|-180.0|180.0|4 +colwalk|-180.2|179.8|4 +colwalk|-179.9|179.9|4 +colwalk|139.5|140.5|11 +colwalk|179.5|-179.5|11 +colwalk|-0.5|0.5|9 +colwalk|179.95|179.96|18 diff --git a/Tests/MapConductorCoreTests/Resources/projection-expected.txt b/Tests/MapConductorCoreTests/Resources/projection-expected.txt new file mode 100644 index 0000000..b58e7ed --- /dev/null +++ b/Tests/MapConductorCoreTests/Resources/projection-expected.txt @@ -0,0 +1,34 @@ +project|0|0|0.000000|-0.000000 +project|35.681236|139.767125|15558805.184640|4256848.120220 +project|51.5074|-0.1278|-14226.630923|6711542.475588 +project|-33.8688|151.2093|16832542.279207|-4011198.647308 +project|-1.2921|36.8219|4098995.158041|-143848.107271 +project|64.1466|-21.9426|-2442639.058680|9387089.649838 +project|0|180|-20037508.342789|-0.000000 +project|0|-180|-20037508.342789|-0.000000 +project|0|90|10018754.171395|-0.000000 +project|0|-90|-10018754.171395|-0.000000 +project|85.05112878|0|0.000000|20037508.343039 +project|-85.05112878|0|0.000000|-20037508.343039 +project|85.1|0|0.000000|20037508.343039 +project|86|0|0.000000|20037508.343039 +project|89|0|0.000000|20037508.343039 +project|89.999|0|0.000000|20037508.343039 +project|90|0|0.000000|20037508.343039 +project|-90|0|0.000000|-20037508.343039 +project|0|190|-18924313.434857|-0.000000 +project|0|-190|18924313.434857|-0.000000 +project|0|360|0.000000|-0.000000 +project|0|-360|0.000000|-0.000000 +project|35.681236|189.767125|-18950236.961275|4256848.120220 +unproject|0|0|0.000000|0.000000 +unproject|15556463.769352|4257881.238407|35.688774|139.746092 +unproject|-14226.343867|6711665.221216|51.508086|-0.127797 +unproject|16832005.123478|-4011119.083890|-33.868207|151.204475 +unproject|20037508.342789|20037508.342789|85.051129|180.000000 +unproject|-20037508.342789|-20037508.342789|-85.051129|-180.000000 +unproject|20037508.342789|0|0.000000|180.000000 +unproject|0|20037508.342789|85.051129|0.000000 +unproject|0|-20037508.342789|-85.051129|0.000000 +unproject|21148622.5|0|0.000000|-170.018692 +unproject|-21148622.5|0|0.000000|170.018692 diff --git a/Tests/MapConductorCoreTests/Resources/projection-vectors.txt b/Tests/MapConductorCoreTests/Resources/projection-vectors.txt new file mode 100644 index 0000000..a1de78e --- /dev/null +++ b/Tests/MapConductorCoreTests/Resources/projection-vectors.txt @@ -0,0 +1,74 @@ +# Web Mercator 適合ベクタ。android-sdk / ios-sdk / react-sdk で同一のファイル。 +# +# ## これは何か +# +# `zoom-golden.txt` とは別物。あちらは移行前のプロバイダが返した値の記録で、 +# 既知の不具合(ios-for-mapbox のオフセット欠落)まで保存している。こちらは +# 「3 プラットフォームの**現在の実装**が同じ答えを出すか」を見るための入力。 +# +# provider に依存しない純粋な計算だけを対象にする。provider ごとのカメラモデル +# の差は正当なので、ここには入れない。 +# +# ## 形式 +# +# project|| 緯度経度 -> EPSG:3857 メートル +# unproject|| EPSG:3857 メートル -> 緯度経度 +# +# 出力は `%.6f`(マイクロメートル / ナノ度)で揃える。f64 のノイズより十分粗く、 +# 実装差より十分細かい。言語ごとの既定の数値表記(Kotlin の 1.49E7 など)を +# 避けるために桁を固定している。 +# +# ## 何を狙った入力か +# +# 端の扱いが実装で割れる。極、日付変更線の外側、Web Mercator の定義端 +# (±85.05112878)を意図的に含めてある。 + +# --- 通常域 --- +project|0|0 +project|35.681236|139.767125 +project|51.5074|-0.1278 +project|-33.8688|151.2093 +project|-1.2921|36.8219 +project|64.1466|-21.9426 + +# --- 赤道と子午線 --- +project|0|180 +project|0|-180 +project|0|90 +project|0|-90 + +# --- Web Mercator の定義端 --- +project|85.05112878|0 +project|-85.05112878|0 + +# --- 定義端の外側。クランプするかどうかで割れる --- +project|85.1|0 +project|86|0 +project|89|0 +project|89.999|0 +project|90|0 +project|-90|0 + +# --- 日付変更線の外側。wrap するかどうかで割れる --- +project|0|190 +project|0|-190 +project|0|360 +project|0|-360 +project|35.681236|189.767125 + +# --- 逆変換:通常域 --- +unproject|0|0 +unproject|15556463.769352|4257881.238407 +unproject|-14226.343867|6711665.221216 +unproject|16832005.123478|-4011119.083890 + +# --- 逆変換:端 --- +unproject|20037508.342789|20037508.342789 +unproject|-20037508.342789|-20037508.342789 +unproject|20037508.342789|0 +unproject|0|20037508.342789 +unproject|0|-20037508.342789 + +# --- 逆変換:範囲外。x が 1 周を超える --- +unproject|21148622.5|0 +unproject|-21148622.5|0 diff --git a/Tests/MapConductorCoreTests/StyleMutationStoreTests.swift b/Tests/MapConductorCoreTests/StyleMutationStoreTests.swift new file mode 100644 index 0000000..9804774 --- /dev/null +++ b/Tests/MapConductorCoreTests/StyleMutationStoreTests.swift @@ -0,0 +1,190 @@ +import XCTest + +@testable import MapConductorCore + +/** + The two things a provider would otherwise have to remember on its own, and + both of which look fine until they do not: a replaced set has to undo what + it dropped, and a style load wipes everything that was applied. + + Kept in step with `StyleMutationStoreTest` on Android. + */ +final class StyleMutationStoreTests: XCTestCase { + private var applied: [StyleMutation] = [] + + override func setUp() { + super.setUp() + applied = [] + } + + private func store(unsupported: @escaping (StyleMutation) -> Bool = { _ in false }) + -> StyleMutationStore + { + StyleMutationStore { [weak self] mutations in + self?.applied.append(contentsOf: mutations) + return mutations.filter(unsupported) + } + } + + private func paint(_ layer: String, _ key: String, _ value: String, _ previous: String) + -> StyleMutation + { + .setPaint(layerId: layer, key: key, value: value, previous: previous) + } + + func testReplacingASetPutsBackWhatTheNewOneNoLongerMentions() { + let store = store() + _ = store.apply([ + paint("roads", "line-color", "\"#fff\"", "\"#888\""), + paint("water", "fill-color", "\"#000\"", "\"#a0c8f0\""), + ]) + applied = [] + + // The second set drops the water rule entirely. + _ = store.apply([paint("roads", "line-color", "\"#f00\"", "\"#888\"")]) + + XCTAssertEqual( + applied, + [ + paint("water", "fill-color", "\"#a0c8f0\"", "\"#000\""), + paint("roads", "line-color", "\"#f00\"", "\"#888\""), + ], + "water goes back to the style's own colour, then roads take the new one" + ) + } + + /// The restore has to be sent before the new value, or a property that + /// both sets touch would be put back to the style's value after being + /// given its new one. + func testRestoresGoOutBeforeTheNewValues() { + let store = store() + _ = store.apply([paint("a", "line-color", "\"#1\"", "\"#0\"")]) + applied = [] + _ = store.apply([paint("b", "line-color", "\"#2\"", "\"#0\"")]) + + XCTAssertEqual(applied.first?.layerId, "a") + XCTAssertEqual(applied.last?.layerId, "b") + } + + func testClearPutsEverythingBack() { + let store = store() + _ = store.apply([paint("roads", "line-color", "\"#fff\"", "\"#888\"")]) + applied = [] + + store.clear() + + XCTAssertEqual(applied, [paint("roads", "line-color", "\"#888\"", "\"#fff\"")]) + XCTAssertTrue(store.applied.isEmpty) + } + + /// A design change, a provider reloading, anything: the renderer comes + /// back with the document's values and nothing that was applied over + /// them. Without this the adjustment survives until the first basemap + /// switch and then quietly stops. + func testAStyleLoadPutsTheWholeSetOnAgain() { + let store = store() + let mutations = [paint("roads", "line-color", "\"#fff\"", "\"#888\"")] + _ = store.apply(mutations) + applied = [] + + store.onStyleLoaded() + + XCTAssertEqual(applied, mutations) + } + + func testNothingAppliedMeansNothingToDoOnAStyleLoad() { + store().onStyleLoaded() + XCTAssertTrue(applied.isEmpty) + } + + /// MapLibre here cannot set every spec key by name. The caller has to be + /// told which ones, rather than being left to assume the map looks the + /// way the rules asked. + func testWhatTheMapCouldNotDoComesBack() { + let store = store(unsupported: { mutation in + if case let .setPaint(_, key, _, _) = mutation { return key == "line-gradient" } + return false + }) + let failed = store.apply([ + paint("roads", "line-color", "\"#fff\"", "\"#888\""), + paint("roads", "line-gradient", "[\"interpolate\"]", "null"), + ]) + XCTAssertEqual(failed.count, 1) + XCTAssertEqual(failed.first?.target, "paint|roads|line-gradient") + } + + func testReadsWhatTheCompilerWrites() { + let parsed = StyleMutation.parseList( + """ + [ + {"op":"setPaint","layerId":"roads","key":"line-color","value":"#fff","previous":null}, + {"op":"setLayout","layerId":"labels","key":"visibility","value":"none","previous":"visible"}, + {"op":"setFilter","layerId":"roads","value":["==","class","motorway"],"previous":null}, + {"op":"setZoomRange","layerId":"roads","minZoom":5,"maxZoom":null, + "previousMinZoom":null,"previousMaxZoom":null}, + {"op":"addLayer","layerId":"bg","layer":{"id":"bg","type":"background"},"beforeId":"water"}, + {"op":"somethingFromTheFuture","layerId":"roads"} + ] + """ + ) + + XCTAssertEqual(parsed.count, 5, "the unknown op is dropped, the rest survive") + // A string value keeps its quotes, or no renderer could read it back. + XCTAssertEqual(parsed[0], paint("roads", "line-color", "\"#fff\"", "null")) + XCTAssertEqual( + parsed[2], + .setFilter(layerId: "roads", value: "[\"==\",\"class\",\"motorway\"]", previous: "null") + ) + XCTAssertEqual( + parsed[3], + .setZoomRange( + layerId: "roads", minZoom: 5, maxZoom: nil, + previousMinZoom: nil, previousMaxZoom: nil + ) + ) + XCTAssertEqual(parsed[4].layerId, "bg") + } + + /// Rubbish in must not take the map down; it means no adjustment. + func testAnswersWithNothingRatherThanThrowing() { + XCTAssertTrue(StyleMutation.parseList("not json").isEmpty) + XCTAssertTrue(StyleMutation.parseList("{}").isEmpty) + XCTAssertTrue(StyleMutation.parseList("[{\"op\":\"setPaint\"}]").isEmpty) + } + + /// Applying a set and then its reverse has to leave the map where it + /// started, whatever the values were. + func testEveryMutationUndoesItself() { + let mutations: [StyleMutation] = [ + paint("a", "line-color", "\"#fff\"", "\"#888\""), + .setLayout(layerId: "b", key: "visibility", value: "\"none\"", previous: "null"), + .setFilter(layerId: "c", value: "[\"all\"]", previous: "null"), + .setZoomRange( + layerId: "d", minZoom: 5, maxZoom: 12, previousMinZoom: nil, previousMaxZoom: 22 + ), + ] + for mutation in mutations { + XCTAssertEqual( + mutation.reversed()?.reversed(), mutation, + "\(mutation.target) did not survive a round trip" + ) + } + } + + func testAddingALayerIsUndoneByRemovingIt() { + let add = StyleMutation.addLayer( + layerId: "bg", layer: #"{"id":"bg","type":"background"}"#, beforeId: nil) + XCTAssertEqual(add.reversed(), .removeLayer(layerId: "bg")) + } + + /// Booleans arrive from `JSONSerialization` as `NSNumber` and must not + /// come out as `1`: a renderer reading back `1` where the style said + /// `true` is a renderer given a different style. + func testWritesBooleansAsBooleans() { + XCTAssertEqual(StyleMutation.jsonText(of: true), "true") + XCTAssertEqual(StyleMutation.jsonText(of: false), "false") + XCTAssertEqual(StyleMutation.jsonText(of: 2), "2") + XCTAssertEqual(StyleMutation.jsonText(of: "it \"quotes\""), "\"it \\\"quotes\\\"\"") + XCTAssertEqual(StyleMutation.jsonText(of: nil), "null") + } +} diff --git a/TilePng.xcframework/Info.plist b/TilePng.xcframework/Info.plist new file mode 100644 index 0000000..4bf8942 --- /dev/null +++ b/TilePng.xcframework/Info.plist @@ -0,0 +1,43 @@ + + + + + AvailableLibraries + + + BinaryPath + libtile_png_ffi.a + LibraryIdentifier + ios-arm64-simulator + LibraryPath + libtile_png_ffi.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + BinaryPath + libtile_png_ffi.a + LibraryIdentifier + ios-arm64 + LibraryPath + libtile_png_ffi.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/TilePng.xcframework/ios-arm64-simulator/libtile_png_ffi.a b/TilePng.xcframework/ios-arm64-simulator/libtile_png_ffi.a new file mode 100644 index 0000000..93eeea7 Binary files /dev/null and b/TilePng.xcframework/ios-arm64-simulator/libtile_png_ffi.a differ diff --git a/TilePng.xcframework/ios-arm64/libtile_png_ffi.a b/TilePng.xcframework/ios-arm64/libtile_png_ffi.a new file mode 100644 index 0000000..a479fc8 Binary files /dev/null and b/TilePng.xcframework/ios-arm64/libtile_png_ffi.a differ diff --git a/api/MapConductorCore.api.swift b/api/MapConductorCore.api.swift index e91740f..a9425df 100644 --- a/api/MapConductorCore.api.swift +++ b/api/MapConductorCore.api.swift @@ -1,8 +1,10 @@ +import CTilePng import Combine import CoreGraphics import CoreLocation import CoreText import Foundation +import Metal import Network import QuartzCore import Swift @@ -13,1188 +15,1203 @@ import _StringProcessing import _SwiftConcurrencyShims import os public enum InternalMapConductorApi { - public static let spiGroup: Swift.String + public static let spiGroup: Swift::String } -@_hasMissingDesignatedInitializers final public class LocalTileServer { - final public var baseUrl: Swift.String { +@_hasMissingDesignatedInitializers final public class LocalTileServer : @unchecked Swift::Sendable { + final public var baseUrl: Swift::String { get } - final public func register(routeId: Swift.String, provider: any MapConductorCore.TileProvider) - final public func unregister(routeId: Swift.String) - final public var isListening: Swift.Bool { + final public func register(routeId: Swift::String, provider: any MapConductorCore::TileProvider) + final public func unregister(routeId: Swift::String) + final public func registerDocument(id: Swift::String, contentType: Swift::String, body: Foundation::Data) + final public func unregisterDocument(id: Swift::String) + final public func documentUrl(id: Swift::String) -> Swift::String + final public func registerFiles(routeId: Swift::String, directory: Foundation::URL, fallback: (@Sendable (_ relativePath: Swift::String) -> Foundation::Data?)? = nil) + final public func unregisterFiles(routeId: Swift::String) + final public func filesUrl(routeId: Swift::String) -> Swift::String + final public var isListening: Swift::Bool { get } - final public func setForceNoStoreCache(_ value: Swift.Bool) - final public func urlTemplate(routeId: Swift.String, tileSize: Swift.Int) -> Swift.String - final public func urlTemplate(routeId: Swift.String, tileSize: Swift.Int, cacheKey: Swift.String) -> Swift.String + final public func setForceNoStoreCache(_ value: Swift::Bool) + final public func urlTemplate(routeId: Swift::String, tileSize: Swift::Int) -> Swift::String + final public func urlTemplate(routeId: Swift::String, tileSize: Swift::Int, cacheKey: Swift::String) -> Swift::String + final public func renderLocalTile(url: Foundation::URL, isCancelled: @escaping @Sendable () -> Swift::Bool = { false }) -> Foundation::Data? @available(*, deprecated, message: "`version` is ignored. Use `urlTemplate(routeId:tileSize:)` instead.") - final public func urlTemplate(routeId: Swift.String, version: Swift.Int64) -> Swift.String + final public func urlTemplate(routeId: Swift::String, version: Swift::Int64) -> Swift::String final public func stop() - public static func startServer(forceNoStoreCache: Swift.Bool = false) -> MapConductorCore.LocalTileServer - @objc deinit -} -public struct Markers : MapConductorCore.MapOverlayItemProtocol { - public init(_ states: [MapConductorCore.MarkerState]) - public func append(to content: inout MapConductorCore.MapViewContent) -} -public struct Circles : MapConductorCore.MapOverlayItemProtocol { - public init(_ states: [MapConductorCore.CircleState]) - public func append(to content: inout MapConductorCore.MapViewContent) -} -public struct Polylines : MapConductorCore.MapOverlayItemProtocol { - public init(_ states: [MapConductorCore.PolylineState]) - public func append(to content: inout MapConductorCore.MapViewContent) -} -public struct Polygons : MapConductorCore.MapOverlayItemProtocol { - public init(_ states: [MapConductorCore.PolygonState]) - public func append(to content: inout MapConductorCore.MapViewContent) -} -public struct GroundImages : MapConductorCore.MapOverlayItemProtocol { - public init(_ states: [MapConductorCore.GroundImageState]) - public func append(to content: inout MapConductorCore.MapViewContent) -} -public struct ForArray : MapConductorCore.MapOverlayItemProtocol where Data : Swift.RandomAccessCollection { - public init(_ data: Data, @MapConductorCore.MapViewContentBuilder content: (Data.Element) -> MapConductorCore.MapViewContent) - public func append(to content: inout MapConductorCore.MapViewContent) -} -public struct GroundImage : MapConductorCore.MapOverlayItemProtocol, Swift.Identifiable { - public let id: Swift.String - public let state: MapConductorCore.GroundImageState - public init(state: MapConductorCore.GroundImageState) - public init(bounds: MapConductorCore.GeoRectBounds, image: UIKit.UIImage, opacity: Swift.Double = 0.5, tileSize: Swift.Int = 512, id: Swift.String? = nil, extra: Any? = nil, onClick: MapConductorCore.OnGroundImageEventHandler? = nil) - public func append(to content: inout MapConductorCore.MapViewContent) - public typealias ID = Swift.String -} -public struct RasterLayer : MapConductorCore.MapOverlayItemProtocol, Swift.Identifiable { - public let id: Swift.String - public let state: MapConductorCore.RasterLayerState - public init(state: MapConductorCore.RasterLayerState) - public init(source: MapConductorCore.RasterLayerSource, opacity: Swift.Double = 1.0, visible: Swift.Bool = true, zIndex: Swift.Int = 0, userAgent: Swift.String = RasterLayerState.defaultUserAgent, id: Swift.String? = nil, extraHeaders: [Swift.String : Swift.String]? = nil) - public func append(to content: inout MapConductorCore.MapViewContent) - public typealias ID = Swift.String -} -public struct Marker : MapConductorCore.MapOverlayItemProtocol, Swift.Identifiable { - public let id: Swift.String - public let state: MapConductorCore.MarkerState - public init(state: MapConductorCore.MarkerState) - public init(position: MapConductorCore.GeoPoint, id: Swift.String? = nil, extra: Any? = nil, icon: (any MapConductorCore.MarkerIconProtocol)? = nil, animation: MapConductorCore.MarkerAnimation? = nil, clickable: Swift.Bool = true, draggable: Swift.Bool = false, zIndex: Swift.Int? = nil, onClick: MapConductorCore.OnMarkerEventHandler? = nil, onDragStart: MapConductorCore.OnMarkerEventHandler? = nil, onDrag: MapConductorCore.OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore.OnMarkerEventHandler? = nil) - public init(position: MapConductorCore.GeoPoint, id: Swift.String? = nil, extra: Any? = nil, icon: MapConductorCore.DefaultMarkerIcon, animation: MapConductorCore.MarkerAnimation? = nil, clickable: Swift.Bool = true, draggable: Swift.Bool = false, zIndex: Swift.Int? = nil, onClick: MapConductorCore.OnMarkerEventHandler? = nil, onDragStart: MapConductorCore.OnMarkerEventHandler? = nil, onDrag: MapConductorCore.OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore.OnMarkerEventHandler? = nil) - public func append(to content: inout MapConductorCore.MapViewContent) - public typealias ID = Swift.String -} -public struct InfoBubble : MapConductorCore.MapOverlayItemProtocol, Swift.Identifiable { - public let id: Swift.String - public let marker: MapConductorCore.MarkerState - public let tailOffset: CoreFoundation.CGPoint - public let useIconMetrics: Swift.Bool - public var swiftUIContent: SwiftUICore.AnyView? { - get - } - public var uiViewContent: UIKit.UIView? { - get - } - public init(marker: MapConductorCore.MarkerState, bubbleColor: SwiftUICore.Color = .white, borderColor: SwiftUICore.Color = .black, borderWidth: CoreFoundation.CGFloat = 2.0, contentPadding: CoreFoundation.CGFloat = 8.0, cornerRadius: CoreFoundation.CGFloat = 4.0, tailSize: CoreFoundation.CGFloat = 8.0, @SwiftUICore.ViewBuilder content: () -> Content) where Content : SwiftUICore.View - public init(position: MapConductorCore.GeoPoint, id: Swift.String? = nil, bubbleColor: SwiftUICore.Color = .white, borderColor: SwiftUICore.Color = .black, borderWidth: CoreFoundation.CGFloat = 2.0, contentPadding: CoreFoundation.CGFloat = 8.0, cornerRadius: CoreFoundation.CGFloat = 4.0, tailSize: CoreFoundation.CGFloat = 8.0, @SwiftUICore.ViewBuilder content: () -> Content) where Content : SwiftUICore.View - public init(marker: MapConductorCore.MarkerState, tailOffset: CoreFoundation.CGPoint = CGPoint(x: 0.5, y: 1.0), uiViewContent: UIKit.UIView) - public init(position: MapConductorCore.GeoPoint, id: Swift.String? = nil, tailOffset: CoreFoundation.CGPoint = CGPoint(x: 0.5, y: 1.0), uiViewContent: UIKit.UIView) - public func append(to content: inout MapConductorCore.MapViewContent) - public typealias ID = Swift.String -} -public struct InfoBubbleCustom : MapConductorCore.MapOverlayItemProtocol, Swift.Identifiable { - public let id: Swift.String - public init(marker: MapConductorCore.MarkerState, tailOffset: CoreFoundation.CGPoint, @SwiftUICore.ViewBuilder content: () -> Content) where Content : SwiftUICore.View - public init(marker: MapConductorCore.MarkerState, tailOffset: CoreFoundation.CGPoint, uiViewContent: UIKit.UIView) - public func append(to content: inout MapConductorCore.MapViewContent) - public typealias ID = Swift.String -} -public struct Polyline : MapConductorCore.MapOverlayItemProtocol, Swift.Identifiable { - public let id: Swift.String - public let state: MapConductorCore.PolylineState - public init(state: MapConductorCore.PolylineState) - public init(points: [any MapConductorCore.GeoPointProtocol], id: Swift.String? = nil, strokeColor: UIKit.UIColor = .black, strokeWidth: Swift.Double = 1.0, geodesic: Swift.Bool = false, zIndex: Swift.Int = 0, extra: Any? = nil, onClick: MapConductorCore.OnPolylineEventHandler? = nil) - public init(bounds: MapConductorCore.GeoRectBounds, id: Swift.String? = nil, strokeColor: UIKit.UIColor = .black, strokeWidth: Swift.Double = 1.0, geodesic: Swift.Bool = false, zIndex: Swift.Int = 0, extra: Any? = nil, onClick: MapConductorCore.OnPolylineEventHandler? = nil) - public func append(to content: inout MapConductorCore.MapViewContent) - public typealias ID = Swift.String -} -public struct Polygon : MapConductorCore.MapOverlayItemProtocol, Swift.Identifiable { - public let id: Swift.String - public let state: MapConductorCore.PolygonState - public init(state: MapConductorCore.PolygonState) - public init(points: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]] = [], id: Swift.String? = nil, strokeColor: UIKit.UIColor = .black, strokeWidth: Swift.Double = 1.0, fillColor: UIKit.UIColor = .clear, geodesic: Swift.Bool = false, zIndex: Swift.Int = 0, extra: Any? = nil, onClick: MapConductorCore.OnPolygonEventHandler? = nil) - public init(bounds: MapConductorCore.GeoRectBounds, id: Swift.String? = nil, strokeColor: UIKit.UIColor = .black, strokeWidth: Swift.Double = 1.0, fillColor: UIKit.UIColor = .clear, geodesic: Swift.Bool = false, zIndex: Swift.Int = 0, extra: Any? = nil, onClick: MapConductorCore.OnPolygonEventHandler? = nil) - public func append(to content: inout MapConductorCore.MapViewContent) - public typealias ID = Swift.String -} -public struct Circle : MapConductorCore.MapOverlayItemProtocol, Swift.Identifiable { - public let id: Swift.String - public let state: MapConductorCore.CircleState - public init(state: MapConductorCore.CircleState) - public init(center: any MapConductorCore.GeoPointProtocol, radiusMeters: Swift.Double, geodesic: Swift.Bool = true, clickable: Swift.Bool = true, strokeColor: UIKit.UIColor = .red, strokeWidth: Swift.Double = 2.0, fillColor: UIKit.UIColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5), id: Swift.String? = nil, zIndex: Swift.Int? = nil, extra: Any? = nil, onClick: MapConductorCore.OnCircleEventHandler? = nil) - public func append(to content: inout MapConductorCore.MapViewContent) - public typealias ID = Swift.String -} -public struct MapUISettings : Swift.Equatable { - public var scrollGesture: Swift.Bool - public var zoomGesture: Swift.Bool - public var rotateGesture: Swift.Bool - public var tiltGesture: Swift.Bool - public init(scrollGesture: Swift.Bool = true, zoomGesture: Swift.Bool = true, rotateGesture: Swift.Bool = true, tiltGesture: Swift.Bool = true) - public static let Default: MapConductorCore.MapUISettings - public static let None: MapConductorCore.MapUISettings - public static func == (a: MapConductorCore.MapUISettings, b: MapConductorCore.MapUISettings) -> Swift.Bool -} -public enum MapGesture : Swift.String, Swift.CaseIterable, Swift.Sendable { + public static func startServer(forceNoStoreCache: Swift::Bool = false) -> MapConductorCore::LocalTileServer + @objc deinit +} +public struct Markers : MapConductorCore::MapOverlayItemProtocol { + public init(_ states: [MapConductorCore::MarkerState]) + public func append(to content: inout MapConductorCore::MapViewContent) +} +public struct Circles : MapConductorCore::MapOverlayItemProtocol { + public init(_ states: [MapConductorCore::CircleState]) + public func append(to content: inout MapConductorCore::MapViewContent) +} +public struct Polylines : MapConductorCore::MapOverlayItemProtocol { + public init(_ states: [MapConductorCore::PolylineState]) + public func append(to content: inout MapConductorCore::MapViewContent) +} +public struct Polygons : MapConductorCore::MapOverlayItemProtocol { + public init(_ states: [MapConductorCore::PolygonState]) + public func append(to content: inout MapConductorCore::MapViewContent) +} +public struct GroundImages : MapConductorCore::MapOverlayItemProtocol { + public init(_ states: [MapConductorCore::GroundImageState]) + public func append(to content: inout MapConductorCore::MapViewContent) +} +public struct ForArray : MapConductorCore::MapOverlayItemProtocol where Data : Swift::RandomAccessCollection { + public init(_ data: Data, @MapConductorCore::MapViewContentBuilder content: (Data.Element) -> MapConductorCore::MapViewContent) + public func append(to content: inout MapConductorCore::MapViewContent) +} +public struct GroundImage : MapConductorCore::MapOverlayItemProtocol, Swift::Identifiable { + public let id: Swift::String + public let state: MapConductorCore::GroundImageState + public init(state: MapConductorCore::GroundImageState) + public init(bounds: MapConductorCore::GeoRectBounds, image: UIKit::UIImage, opacity: Swift::Double = 0.5, tileSize: Swift::Int = 512, id: Swift::String? = nil, extra: Any? = nil, onClick: MapConductorCore::OnGroundImageEventHandler? = nil) + public func append(to content: inout MapConductorCore::MapViewContent) + public typealias ID = Swift::String +} +public struct RasterLayer : MapConductorCore::MapOverlayItemProtocol, Swift::Identifiable { + public let id: Swift::String + public let state: MapConductorCore::RasterLayerState + public init(state: MapConductorCore::RasterLayerState) + public init(source: MapConductorCore::RasterLayerSource, opacity: Swift::Double = 1.0, visible: Swift::Bool = true, zIndex: Swift::Int = 0, userAgent: Swift::String = RasterLayerState.defaultUserAgent, id: Swift::String? = nil, extraHeaders: [Swift::String : Swift::String]? = nil) + public func append(to content: inout MapConductorCore::MapViewContent) + public typealias ID = Swift::String +} +public struct Marker : MapConductorCore::MapOverlayItemProtocol, Swift::Identifiable { + public let id: Swift::String + public let state: MapConductorCore::MarkerState + public init(state: MapConductorCore::MarkerState) + public init(position: MapConductorCore::GeoPoint, id: Swift::String? = nil, extra: Any? = nil, icon: (any MapConductorCore::MarkerIconProtocol)? = nil, animation: MapConductorCore::MarkerAnimation? = nil, clickable: Swift::Bool = true, draggable: Swift::Bool = false, zIndex: Swift::Int? = nil, onClick: MapConductorCore::OnMarkerEventHandler? = nil, onDragStart: MapConductorCore::OnMarkerEventHandler? = nil, onDrag: MapConductorCore::OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore::OnMarkerEventHandler? = nil) + public init(position: MapConductorCore::GeoPoint, id: Swift::String? = nil, extra: Any? = nil, icon: MapConductorCore::DefaultMarkerIcon, animation: MapConductorCore::MarkerAnimation? = nil, clickable: Swift::Bool = true, draggable: Swift::Bool = false, zIndex: Swift::Int? = nil, onClick: MapConductorCore::OnMarkerEventHandler? = nil, onDragStart: MapConductorCore::OnMarkerEventHandler? = nil, onDrag: MapConductorCore::OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore::OnMarkerEventHandler? = nil) + public func append(to content: inout MapConductorCore::MapViewContent) + public typealias ID = Swift::String +} +public struct InfoBubble : MapConductorCore::MapOverlayItemProtocol, Swift::Identifiable { + public let id: Swift::String + public let marker: MapConductorCore::MarkerState + public let tailOffset: CoreFoundation::CGPoint + public let useIconMetrics: Swift::Bool + public var swiftUIContent: SwiftUICore::AnyView? { + get + } + public var uiViewContent: UIKit::UIView? { + get + } + public init(marker: MapConductorCore::MarkerState, bubbleColor: SwiftUICore::Color = .white, borderColor: SwiftUICore::Color = .black, borderWidth: CoreFoundation::CGFloat = 2.0, contentPadding: CoreFoundation::CGFloat = 8.0, cornerRadius: CoreFoundation::CGFloat = 4.0, tailSize: CoreFoundation::CGFloat = 8.0, @SwiftUICore::ViewBuilder content: () -> Content) where Content : SwiftUICore::View + public init(position: MapConductorCore::GeoPoint, id: Swift::String? = nil, bubbleColor: SwiftUICore::Color = .white, borderColor: SwiftUICore::Color = .black, borderWidth: CoreFoundation::CGFloat = 2.0, contentPadding: CoreFoundation::CGFloat = 8.0, cornerRadius: CoreFoundation::CGFloat = 4.0, tailSize: CoreFoundation::CGFloat = 8.0, @SwiftUICore::ViewBuilder content: () -> Content) where Content : SwiftUICore::View + public init(marker: MapConductorCore::MarkerState, tailOffset: CoreFoundation::CGPoint = CGPoint(x: 0.5, y: 1.0), uiViewContent: UIKit::UIView) + public init(position: MapConductorCore::GeoPoint, id: Swift::String? = nil, tailOffset: CoreFoundation::CGPoint = CGPoint(x: 0.5, y: 1.0), uiViewContent: UIKit::UIView) + public func append(to content: inout MapConductorCore::MapViewContent) + public typealias ID = Swift::String +} +public struct InfoBubbleCustom : MapConductorCore::MapOverlayItemProtocol, Swift::Identifiable { + public let id: Swift::String + public init(marker: MapConductorCore::MarkerState, tailOffset: CoreFoundation::CGPoint, @SwiftUICore::ViewBuilder content: () -> Content) where Content : SwiftUICore::View + public init(marker: MapConductorCore::MarkerState, tailOffset: CoreFoundation::CGPoint, uiViewContent: UIKit::UIView) + public func append(to content: inout MapConductorCore::MapViewContent) + public typealias ID = Swift::String +} +public struct Polyline : MapConductorCore::MapOverlayItemProtocol, Swift::Identifiable { + public let id: Swift::String + public let state: MapConductorCore::PolylineState + public init(state: MapConductorCore::PolylineState) + public init(points: [any MapConductorCore::GeoPointProtocol], id: Swift::String? = nil, strokeColor: UIKit::UIColor = .black, strokeWidth: Swift::Double = 1.0, geodesic: Swift::Bool = false, zIndex: Swift::Int = 0, extra: Any? = nil, onClick: MapConductorCore::OnPolylineEventHandler? = nil) + public init(bounds: MapConductorCore::GeoRectBounds, id: Swift::String? = nil, strokeColor: UIKit::UIColor = .black, strokeWidth: Swift::Double = 1.0, geodesic: Swift::Bool = false, zIndex: Swift::Int = 0, extra: Any? = nil, onClick: MapConductorCore::OnPolylineEventHandler? = nil) + public func append(to content: inout MapConductorCore::MapViewContent) + public typealias ID = Swift::String +} +public struct Polygon : MapConductorCore::MapOverlayItemProtocol, Swift::Identifiable { + public let id: Swift::String + public let state: MapConductorCore::PolygonState + public init(state: MapConductorCore::PolygonState) + public init(points: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]] = [], id: Swift::String? = nil, strokeColor: UIKit::UIColor = .black, strokeWidth: Swift::Double = 1.0, fillColor: UIKit::UIColor = .clear, geodesic: Swift::Bool = false, zIndex: Swift::Int = 0, extra: Any? = nil, onClick: MapConductorCore::OnPolygonEventHandler? = nil) + public init(bounds: MapConductorCore::GeoRectBounds, id: Swift::String? = nil, strokeColor: UIKit::UIColor = .black, strokeWidth: Swift::Double = 1.0, fillColor: UIKit::UIColor = .clear, geodesic: Swift::Bool = false, zIndex: Swift::Int = 0, extra: Any? = nil, onClick: MapConductorCore::OnPolygonEventHandler? = nil) + public func append(to content: inout MapConductorCore::MapViewContent) + public typealias ID = Swift::String +} +public struct Circle : MapConductorCore::MapOverlayItemProtocol, Swift::Identifiable { + public let id: Swift::String + public let state: MapConductorCore::CircleState + public init(state: MapConductorCore::CircleState) + public init(center: any MapConductorCore::GeoPointProtocol, radiusMeters: Swift::Double, geodesic: Swift::Bool = true, clickable: Swift::Bool = true, strokeColor: UIKit::UIColor = .red, strokeWidth: Swift::Double = 2.0, fillColor: UIKit::UIColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5), id: Swift::String? = nil, zIndex: Swift::Int? = nil, extra: Any? = nil, onClick: MapConductorCore::OnCircleEventHandler? = nil) + public func append(to content: inout MapConductorCore::MapViewContent) + public typealias ID = Swift::String +} +public struct MapUISettings : Swift::Equatable { + public var scrollGesture: Swift::Bool + public var zoomGesture: Swift::Bool + public var rotateGesture: Swift::Bool + public var tiltGesture: Swift::Bool + public init(scrollGesture: Swift::Bool = true, zoomGesture: Swift::Bool = true, rotateGesture: Swift::Bool = true, tiltGesture: Swift::Bool = true) + public static let Default: MapConductorCore::MapUISettings + public static let None: MapConductorCore::MapUISettings + public static func == (a: MapConductorCore::MapUISettings, b: MapConductorCore::MapUISettings) -> Swift::Bool +} +public enum MapGesture : Swift::String, Swift::CaseIterable, Swift::Sendable { case scroll case zoom case rotate case tilt - public var settingName: Swift.String { + public var settingName: Swift::String { get } - public init?(rawValue: Swift.String) - public typealias AllCases = [MapConductorCore.MapGesture] - public typealias RawValue = Swift.String - nonisolated public static var allCases: [MapConductorCore.MapGesture] { + public init?(rawValue: Swift::String) + public typealias AllCases = [MapConductorCore::MapGesture] + public typealias RawValue = Swift::String + nonisolated public static var allCases: [MapConductorCore::MapGesture] { get } - public var rawValue: Swift.String { + public var rawValue: Swift::String { get } } public enum MapUISettingsDiagnostics { - public static func warnIfRequested(_ requested: Swift.Bool, gesture: MapConductorCore.MapGesture, provider: Swift.String, reason: Swift.String) + public static func warnIfRequested(_ requested: Swift::Bool, gesture: MapConductorCore::MapGesture, provider: Swift::String, reason: Swift::String) public static func resetWarnings() } -public struct MapViewHandlers where State : MapConductorCore.MapViewStateProtocol { - public let onMapLoaded: MapConductorCore.OnMapLoadedHandler? - public let onMapClick: MapConductorCore.OnMapEventHandler? - public let onMapLongClick: MapConductorCore.OnMapEventHandler? - public let onCameraMoveStart: MapConductorCore.OnCameraMoveHandler? - public let onCameraMove: MapConductorCore.OnCameraMoveHandler? - public let onCameraMoveEnd: MapConductorCore.OnCameraMoveHandler? - public let sdkInitialize: (() -> Swift.Void)? - public init(onMapLoaded: MapConductorCore.OnMapLoadedHandler? = nil, onMapClick: MapConductorCore.OnMapEventHandler? = nil, onMapLongClick: MapConductorCore.OnMapEventHandler? = nil, onCameraMoveStart: MapConductorCore.OnCameraMoveHandler? = nil, onCameraMove: MapConductorCore.OnCameraMoveHandler? = nil, onCameraMoveEnd: MapConductorCore.OnCameraMoveHandler? = nil, sdkInitialize: (() -> Swift.Void)? = nil) +public struct MapViewHandlers where State : MapConductorCore::MapViewStateProtocol { + public let onMapLoaded: MapConductorCore::OnMapLoadedHandler? + public let onMapClick: MapConductorCore::OnMapEventHandler? + public let onMapLongClick: MapConductorCore::OnMapEventHandler? + public let onCameraMoveStart: MapConductorCore::OnCameraMoveHandler? + public let onCameraMove: MapConductorCore::OnCameraMoveHandler? + public let onCameraMoveEnd: MapConductorCore::OnCameraMoveHandler? + public let sdkInitialize: (() -> Swift::Void)? + public init(onMapLoaded: MapConductorCore::OnMapLoadedHandler? = nil, onMapClick: MapConductorCore::OnMapEventHandler? = nil, onMapLongClick: MapConductorCore::OnMapEventHandler? = nil, onCameraMoveStart: MapConductorCore::OnCameraMoveHandler? = nil, onCameraMove: MapConductorCore::OnCameraMoveHandler? = nil, onCameraMoveEnd: MapConductorCore::OnCameraMoveHandler? = nil, sdkInitialize: (() -> Swift::Void)? = nil) } -@_Concurrency.MainActor @preconcurrency public struct MapViewBase : SwiftUICore.View where MapContent : SwiftUICore.View, TopContent : SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public init(attributionRules: [MapConductorCore.AttributionRule], camera: any MapConductorCore.MapCameraPositionProtocol, content: MapConductorCore.MapViewContent, @SwiftUICore.ViewBuilder mapContent: @escaping () -> MapContent, @SwiftUICore.ViewBuilder topContent: @escaping () -> TopContent) - @_Concurrency.MainActor @preconcurrency public var body: some SwiftUICore.View { +@_Concurrency::MainActor @preconcurrency public struct MapViewBase : SwiftUICore::View where MapContent : SwiftUICore::View, TopContent : SwiftUICore::View { + @_Concurrency::MainActor @preconcurrency public init(attributionRules: [MapConductorCore::AttributionRule], camera: any MapConductorCore::MapCameraPositionProtocol, content: MapConductorCore::MapViewContent, @SwiftUICore::ViewBuilder mapContent: @escaping () -> MapContent, @SwiftUICore::ViewBuilder topContent: @escaping () -> TopContent) + @_Concurrency::MainActor @preconcurrency public var body: some SwiftUICore::View { get } public typealias Body = @_opaqueReturnTypeOf("$s16MapConductorCore0A8ViewBaseV4bodyQrvp", 0) __ } -extension MapConductorCore.MapViewBase where TopContent == SwiftUICore.EmptyView { - @_Concurrency.MainActor @preconcurrency public init(attributionRules: [MapConductorCore.AttributionRule], camera: any MapConductorCore.MapCameraPositionProtocol, content: MapConductorCore.MapViewContent, @SwiftUICore.ViewBuilder mapContent: @escaping () -> MapContent) +extension MapConductorCore::MapViewBase where TopContent == SwiftUICore::EmptyView { + @_Concurrency::MainActor @preconcurrency public init(attributionRules: [MapConductorCore::AttributionRule], camera: any MapConductorCore::MapCameraPositionProtocol, content: MapConductorCore::MapViewContent, @SwiftUICore::ViewBuilder mapContent: @escaping () -> MapContent) } -@_Concurrency.MainActor open class MapViewCoordinatorBase : ObjectiveC.NSObject where State : MapConductorCore.MapViewStateProtocol { - @_Concurrency.MainActor final public let state: State - @_Concurrency.MainActor final public let handlers: MapConductorCore.MapViewHandlers - @_Concurrency.MainActor final public let infoBubbleContainer: MapConductorCore.PassthroughContainerView - @_Concurrency.MainActor public var didCallMapLoaded: Swift.Bool { +@_Concurrency::MainActor open class MapViewCoordinatorBase : ObjectiveC::NSObject where State : MapConductorCore::MapViewStateProtocol { + @_Concurrency::MainActor final public let state: State + @_Concurrency::MainActor final public let handlers: MapConductorCore::MapViewHandlers + @_Concurrency::MainActor final public let infoBubbleContainer: MapConductorCore::PassthroughContainerView + @_Concurrency::MainActor public var didCallMapLoaded: Swift::Bool { get } - @_Concurrency.MainActor public init(state: State, handlers: MapConductorCore.MapViewHandlers) - @_Concurrency.MainActor public func screenProjectionGate(feature: Swift.String) -> () -> Swift.Bool - @_Concurrency.MainActor public var onMapLoaded: MapConductorCore.OnMapLoadedHandler? { + @_Concurrency::MainActor public init(state: State, handlers: MapConductorCore::MapViewHandlers) + @_Concurrency::MainActor public var styleHost: MapConductorCore::MapViewStyleHost { get } - @_Concurrency.MainActor public var onMapClick: MapConductorCore.OnMapEventHandler? { + @_Concurrency::MainActor public func screenProjectionGate(feature: Swift::String) -> () -> Swift::Bool + @_Concurrency::MainActor public var onMapLoaded: MapConductorCore::OnMapLoadedHandler? { get } - @_Concurrency.MainActor public var onMapLongClick: MapConductorCore.OnMapEventHandler? { + @_Concurrency::MainActor public var onMapClick: MapConductorCore::OnMapEventHandler? { get } - @_Concurrency.MainActor public var onCameraMoveStart: MapConductorCore.OnCameraMoveHandler? { + @_Concurrency::MainActor public var onMapLongClick: MapConductorCore::OnMapEventHandler? { get } - @_Concurrency.MainActor public var onCameraMove: MapConductorCore.OnCameraMoveHandler? { + @_Concurrency::MainActor public var onCameraMoveStart: MapConductorCore::OnCameraMoveHandler? { get } - @_Concurrency.MainActor public var onCameraMoveEnd: MapConductorCore.OnCameraMoveHandler? { + @_Concurrency::MainActor public var onCameraMove: MapConductorCore::OnCameraMoveHandler? { get } - @_Concurrency.MainActor public static func runOnce(_ initializer: () -> Swift.Void) - @_Concurrency.MainActor public func performMapLoadedOnce(_ body: () -> Swift.Void) - @_Concurrency.MainActor public func applyCameraRestriction(_ restriction: MapConductorCore.CameraRestriction?, to controller: (any MapConductorCore.MapViewControllerProtocol)?) - @_Concurrency.MainActor public func reapplyCameraRestriction(to controller: (any MapConductorCore.MapViewControllerProtocol)?) - @_Concurrency.MainActor public func attachInfoBubbleContainer(to hostView: UIKit.UIView) + @_Concurrency::MainActor public var onCameraMoveEnd: MapConductorCore::OnCameraMoveHandler? { + get + } + @_Concurrency::MainActor public static func runOnce(_ initializer: () -> Swift::Void) + @_Concurrency::MainActor public func performMapLoadedOnce(_ body: () -> Swift::Void) + @_Concurrency::MainActor public func applyCameraRestriction(_ restriction: MapConductorCore::CameraRestriction?, to controller: (any MapConductorCore::MapViewControllerProtocol)?) + @_Concurrency::MainActor public func reapplyCameraRestriction(to controller: (any MapConductorCore::MapViewControllerProtocol)?) + @_Concurrency::MainActor public func attachInfoBubbleContainer(to hostView: UIKit::UIView) @objc deinit } -public typealias OnMapLoadedHandler = (State) -> Swift.Void where State : MapConductorCore.MapViewStateProtocol -public typealias OnMapEventHandler = (MapConductorCore.GeoPoint) -> Swift.Void -public typealias OnCameraMoveHandler = (MapConductorCore.MapCameraPosition) -> Swift.Void +public typealias OnMapLoadedHandler = (State) -> Swift::Void where State : MapConductorCore::MapViewStateProtocol +public typealias OnMapEventHandler = (MapConductorCore::GeoPoint) -> Swift::Void +public typealias OnCameraMoveHandler = (MapConductorCore::MapCameraPosition) -> Swift::Void public protocol MapOverlayItemProtocol { - func append(to content: inout MapConductorCore.MapViewContent) + func append(to content: inout MapConductorCore::MapViewContent) } -public protocol ViewBasedMapOverlay : MapConductorCore.MapOverlayItemProtocol, SwiftUICore.View { +public protocol ViewBasedMapOverlay : MapConductorCore::MapOverlayItemProtocol, SwiftUICore::View { } public struct MapViewContent { - public var markers: [MapConductorCore.Marker] - public var infoBubbles: [MapConductorCore.InfoBubble] - public var polylines: [MapConductorCore.Polyline] - public var polygons: [MapConductorCore.Polygon] - public var circles: [MapConductorCore.Circle] - public var groundImages: [MapConductorCore.GroundImage] - public var rasterLayers: [MapConductorCore.RasterLayer] - public var views: [SwiftUICore.AnyView] - public var markerTilingOptions: MapConductorCore.MarkerTilingOptions - public var polygonSyncHandlers: [any MapConductorCore.PolygonSyncHandler] + public var markers: [MapConductorCore::Marker] + public var infoBubbles: [MapConductorCore::InfoBubble] + public var polylines: [MapConductorCore::Polyline] + public var polygons: [MapConductorCore::Polygon] + public var circles: [MapConductorCore::Circle] + public var groundImages: [MapConductorCore::GroundImage] + public var rasterLayers: [MapConductorCore::RasterLayer] + public var views: [SwiftUICore::AnyView] + public var markerTilingOptions: MapConductorCore::MarkerTilingOptions + public var polygonSyncHandlers: [any MapConductorCore::PolygonSyncHandler] public init() } @_functionBuilder public enum MapViewContentBuilder { - public static func buildBlock() -> MapConductorCore.MapViewContent - public static func buildBlock(_ components: MapConductorCore.MapViewContent...) -> MapConductorCore.MapViewContent - public static func buildOptional(_ component: MapConductorCore.MapViewContent?) -> MapConductorCore.MapViewContent - public static func buildEither(first component: MapConductorCore.MapViewContent) -> MapConductorCore.MapViewContent - public static func buildEither(second component: MapConductorCore.MapViewContent) -> MapConductorCore.MapViewContent - public static func buildArray(_ components: [MapConductorCore.MapViewContent]) -> MapConductorCore.MapViewContent - public static func buildExpression(_ expression: T) -> MapConductorCore.MapViewContent where T : MapConductorCore.ViewBasedMapOverlay - @_disfavoredOverload public static func buildExpression(_ expression: any MapConductorCore.MapOverlayItemProtocol) -> MapConductorCore.MapViewContent - public static func buildExpression(_ expression: MapConductorCore.MapViewContent) -> MapConductorCore.MapViewContent + public static func buildBlock() -> MapConductorCore::MapViewContent + public static func buildBlock(_ components: MapConductorCore::MapViewContent...) -> MapConductorCore::MapViewContent + public static func buildOptional(_ component: MapConductorCore::MapViewContent?) -> MapConductorCore::MapViewContent + public static func buildEither(first component: MapConductorCore::MapViewContent) -> MapConductorCore::MapViewContent + public static func buildEither(second component: MapConductorCore::MapViewContent) -> MapConductorCore::MapViewContent + public static func buildArray(_ components: [MapConductorCore::MapViewContent]) -> MapConductorCore::MapViewContent + public static func buildExpression(_ expression: T) -> MapConductorCore::MapViewContent where T : MapConductorCore::ViewBasedMapOverlay + @_disfavoredOverload public static func buildExpression(_ expression: any MapConductorCore::MapOverlayItemProtocol) -> MapConductorCore::MapViewContent + public static func buildExpression(_ expression: MapConductorCore::MapViewContent) -> MapConductorCore::MapViewContent } public protocol TileProvider : AnyObject { - func renderTile(request: MapConductorCore.TileRequest) -> Foundation.Data? + func renderTile(request: MapConductorCore::TileRequest) -> Foundation::Data? + func renderTile(request: MapConductorCore::TileRequest, isCancelled: () -> Swift::Bool) throws -> Foundation::Data? +} +extension MapConductorCore::TileProvider { + public func renderTile(request: MapConductorCore::TileRequest, isCancelled: () -> Swift::Bool) throws -> Foundation::Data? } -public struct TileRequest : Swift.Hashable { - public let x: Swift.Int - public let y: Swift.Int - public let z: Swift.Int - public let pixelRatio: Swift.Int - public init(x: Swift.Int, y: Swift.Int, z: Swift.Int, pixelRatio: Swift.Int = 1) - public static func == (a: MapConductorCore.TileRequest, b: MapConductorCore.TileRequest) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public struct TileRequest : Swift::Hashable { + public let x: Swift::Int + public let y: Swift::Int + public let z: Swift::Int + public let pixelRatio: Swift::Int + public let sourceZoomOffset: Swift::Int + public init(x: Swift::Int, y: Swift::Int, z: Swift::Int, pixelRatio: Swift::Int = 1, sourceZoomOffset: Swift::Int = 0) + public static func == (a: MapConductorCore::TileRequest, b: MapConductorCore::TileRequest) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public enum TileServerRegistry { - public static func get() -> MapConductorCore.LocalTileServer - public static func get(forceNoStoreCache: Swift.Bool) -> MapConductorCore.LocalTileServer - public static func setForceNoStoreCache(_ value: Swift.Bool) + public static func get() -> MapConductorCore::LocalTileServer + public static func get(forceNoStoreCache: Swift::Bool) -> MapConductorCore::LocalTileServer + public static func setForceNoStoreCache(_ value: Swift::Bool) } -public typealias Long = Swift.Int64 +public enum TransparentTile { + public static func png(size: Swift::Int) -> Foundation::Data? +} +public typealias Long = Swift::Int64 final public class CoroutineScope { public init() @objc deinit } -open class CircleController : MapConductorCore.OverlayControllerProtocol, MapConductorCore.SlottedOverlayController where ActualCircle == Renderer.ActualCircle, Renderer : MapConductorCore.CircleOverlayRendererProtocol { - public typealias StateType = MapConductorCore.CircleState - public typealias EntityType = MapConductorCore.CircleEntity - public typealias EventType = MapConductorCore.CircleEvent - final public let circleManager: MapConductorCore.CircleManager +open class CircleController : MapConductorCore::OverlayControllerProtocol, MapConductorCore::SlottedOverlayController where ActualCircle == Renderer.ActualCircle, Renderer : MapConductorCore::CircleOverlayRendererProtocol { + public typealias StateType = MapConductorCore::CircleState + public typealias EntityType = MapConductorCore::CircleEntity + public typealias EventType = MapConductorCore::CircleEvent + final public let circleManager: MapConductorCore::CircleManager open var renderer: Renderer - final public let zIndex: Swift.Int - public var clickListener: ((MapConductorCore.CircleEvent) -> Swift.Void)? - public init(circleManager: MapConductorCore.CircleManager, renderer: Renderer, clickListener: ((MapConductorCore.CircleEvent) -> Swift.Void)? = nil) - public func dispatchClick(event: MapConductorCore.CircleEvent) - open func add(data: [MapConductorCore.CircleState]) async - open func update(state: MapConductorCore.CircleState) async + final public let zIndex: Swift::Int + public var clickListener: ((MapConductorCore::CircleEvent) -> Swift::Void)? + public init(circleManager: MapConductorCore::CircleManager, renderer: Renderer, clickListener: ((MapConductorCore::CircleEvent) -> Swift::Void)? = nil) + public func dispatchClick(event: MapConductorCore::CircleEvent) + open func add(data: [MapConductorCore::CircleState]) async + open func update(state: MapConductorCore::CircleState) async open func clear() async - open func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.CircleEntity? - open func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + open func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::CircleEntity? + open func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async open func destroy() - public var kind: MapConductorCore.OverlayKind { + public var kind: MapConductorCore::OverlayKind { get } - public func hasId(_ id: Swift.String) -> Swift.Bool - public func resolveTap(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.OverlayHit? + public func hasId(_ id: Swift::String) -> Swift::Bool + public func resolveTap(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::OverlayHit? @objc deinit } public protocol CircleEntityProtocol { associatedtype ActualCircle var circle: Self.ActualCircle? { get set } - var state: MapConductorCore.CircleState { get } - var fingerPrint: MapConductorCore.CircleFingerPrint { get } + var state: MapConductorCore::CircleState { get } + var fingerPrint: MapConductorCore::CircleFingerPrint { get } } -final public class CircleEntity : MapConductorCore.CircleEntityProtocol { +final public class CircleEntity : MapConductorCore::CircleEntityProtocol { final public var circle: ActualCircle? - final public let state: MapConductorCore.CircleState - final public let fingerPrint: MapConductorCore.CircleFingerPrint - public init(circle: ActualCircle?, state: MapConductorCore.CircleState) + final public let state: MapConductorCore::CircleState + final public let fingerPrint: MapConductorCore::CircleFingerPrint + public init(circle: ActualCircle?, state: MapConductorCore::CircleState) @objc deinit } public protocol CircleManagerProtocol { associatedtype ActualCircle - func registerEntity(_ entity: MapConductorCore.CircleEntity) - func removeEntity(_ id: Swift.String) -> MapConductorCore.CircleEntity? - func getEntity(_ id: Swift.String) -> MapConductorCore.CircleEntity? - func hasEntity(_ id: Swift.String) -> Swift.Bool - func allEntities() -> [MapConductorCore.CircleEntity] + func registerEntity(_ entity: MapConductorCore::CircleEntity) + func removeEntity(_ id: Swift::String) -> MapConductorCore::CircleEntity? + func getEntity(_ id: Swift::String) -> MapConductorCore::CircleEntity? + func hasEntity(_ id: Swift::String) -> Swift::Bool + func allEntities() -> [MapConductorCore::CircleEntity] func clear() - func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.CircleEntity? + func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::CircleEntity? } -final public class CircleManager : MapConductorCore.CircleManagerProtocol { +final public class CircleManager : MapConductorCore::CircleManagerProtocol { public init() - final public func registerEntity(_ entity: MapConductorCore.CircleEntity) - final public func removeEntity(_ id: Swift.String) -> MapConductorCore.CircleEntity? - final public func getEntity(_ id: Swift.String) -> MapConductorCore.CircleEntity? - final public func hasEntity(_ id: Swift.String) -> Swift.Bool - final public func allEntities() -> [MapConductorCore.CircleEntity] + final public func registerEntity(_ entity: MapConductorCore::CircleEntity) + final public func removeEntity(_ id: Swift::String) -> MapConductorCore::CircleEntity? + final public func getEntity(_ id: Swift::String) -> MapConductorCore::CircleEntity? + final public func hasEntity(_ id: Swift::String) -> Swift::Bool + final public func allEntities() -> [MapConductorCore::CircleEntity] final public func clear() final public func destroy() - final public var isDestroyed: Swift.Bool { + final public var isDestroyed: Swift::Bool { get } - final public func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.CircleEntity? + final public func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::CircleEntity? @objc deinit } public protocol CircleOverlayRendererProtocol { associatedtype ActualCircle - func onAdd(data: [MapConductorCore.CircleOverlayAddParams]) async -> [Self.ActualCircle?] - func onChange(data: [MapConductorCore.CircleOverlayChangeParams]) async -> [Self.ActualCircle?] - func onRemove(data: [MapConductorCore.CircleEntity]) async + func onAdd(data: [MapConductorCore::CircleOverlayAddParams]) async -> [Self.ActualCircle?] + func onChange(data: [MapConductorCore::CircleOverlayChangeParams]) async -> [Self.ActualCircle?] + func onRemove(data: [MapConductorCore::CircleEntity]) async func onPostProcess() async } -open class AbstractCircleOverlayRenderer : MapConductorCore.CircleOverlayRendererProtocol { +open class AbstractCircleOverlayRenderer : MapConductorCore::CircleOverlayRendererProtocol { public init() open func onPostProcess() async - open func createCircle(state: MapConductorCore.CircleState) async -> ActualCircle? - open func updateCircleProperties(circle: ActualCircle, current: MapConductorCore.CircleEntity, prev: MapConductorCore.CircleEntity) async -> ActualCircle? - open func removeCircle(entity: MapConductorCore.CircleEntity) async - public func onAdd(data: [MapConductorCore.CircleOverlayAddParams]) async -> [ActualCircle?] - public func onChange(data: [MapConductorCore.CircleOverlayChangeParams]) async -> [ActualCircle?] - public func onRemove(data: [MapConductorCore.CircleEntity]) async + open func createCircle(state: MapConductorCore::CircleState) async -> ActualCircle? + open func updateCircleProperties(circle: ActualCircle, current: MapConductorCore::CircleEntity, prev: MapConductorCore::CircleEntity) async -> ActualCircle? + open func removeCircle(entity: MapConductorCore::CircleEntity) async + public func onAdd(data: [MapConductorCore::CircleOverlayAddParams]) async -> [ActualCircle?] + public func onChange(data: [MapConductorCore::CircleOverlayChangeParams]) async -> [ActualCircle?] + public func onRemove(data: [MapConductorCore::CircleEntity]) async @objc deinit } public struct CircleOverlayAddParams { - public let state: MapConductorCore.CircleState - public init(state: MapConductorCore.CircleState) + public let state: MapConductorCore::CircleState + public init(state: MapConductorCore::CircleState) } public struct CircleOverlayChangeParams { - public let current: MapConductorCore.CircleEntity - public let prev: MapConductorCore.CircleEntity - public init(current: MapConductorCore.CircleEntity, prev: MapConductorCore.CircleEntity) -} -public struct CircleFingerPrint : Swift.Equatable, Swift.Hashable { - public let id: Swift.Int - public let center: Swift.Int - public let radiusMeters: Swift.Int - public let clickable: Swift.Int - public let geodesic: Swift.Int - public let strokeColor: Swift.Int - public let strokeWidth: Swift.Int - public let fillColor: Swift.Int - public let zIndex: Swift.Int - public let extra: Swift.Int - public static func == (a: MapConductorCore.CircleFingerPrint, b: MapConductorCore.CircleFingerPrint) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { + public let current: MapConductorCore::CircleEntity + public let prev: MapConductorCore::CircleEntity + public init(current: MapConductorCore::CircleEntity, prev: MapConductorCore::CircleEntity) +} +public struct CircleFingerPrint : Swift::Equatable, Swift::Hashable { + public let id: Swift::Int + public let center: Swift::Int + public let radiusMeters: Swift::Int + public let clickable: Swift::Int + public let geodesic: Swift::Int + public let strokeColor: Swift::Int + public let strokeWidth: Swift::Int + public let fillColor: Swift::Int + public let zIndex: Swift::Int + public let extra: Swift::Int + public static func == (a: MapConductorCore::CircleFingerPrint, b: MapConductorCore::CircleFingerPrint) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public struct CircleEvent { - public let state: MapConductorCore.CircleState - public let clicked: any MapConductorCore.GeoPointProtocol - public init(state: MapConductorCore.CircleState, clicked: any MapConductorCore.GeoPointProtocol) + public let state: MapConductorCore::CircleState + public let clicked: any MapConductorCore::GeoPointProtocol + public init(state: MapConductorCore::CircleState, clicked: any MapConductorCore::GeoPointProtocol) } -public typealias OnCircleEventHandler = (MapConductorCore.CircleEvent) -> Swift.Void -final public class CircleState : Combine.ObservableObject, Swift.Identifiable, Swift.Equatable, Swift.Hashable { - final public let id: Swift.String - @Combine.Published @_projectedValueProperty($center) final public var center: any MapConductorCore.GeoPointProtocol { +public typealias OnCircleEventHandler = (MapConductorCore::CircleEvent) -> Swift::Void +final public class CircleState : Combine::ObservableObject, Swift::Identifiable, Swift::Equatable, Swift::Hashable { + final public let id: Swift::String + final public let mutations: MapConductorCore::StateMutationSignal + @Combine::Published @_projectedValueProperty($center) final public var center: any MapConductorCore::GeoPointProtocol { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $center: Combine.Published.Publisher { + final public var $center: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($radiusMeters) final public var radiusMeters: Swift.Double { + @Combine::Published @_projectedValueProperty($radiusMeters) final public var radiusMeters: Swift::Double { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $radiusMeters: Combine.Published.Publisher { + final public var $radiusMeters: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($geodesic) final public var geodesic: Swift.Bool { + @Combine::Published @_projectedValueProperty($geodesic) final public var geodesic: Swift::Bool { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $geodesic: Combine.Published.Publisher { + final public var $geodesic: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($clickable) final public var clickable: Swift.Bool { + @Combine::Published @_projectedValueProperty($clickable) final public var clickable: Swift::Bool { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $clickable: Combine.Published.Publisher { + final public var $clickable: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($strokeColor) final public var strokeColor: UIKit.UIColor { + @Combine::Published @_projectedValueProperty($strokeColor) final public var strokeColor: UIKit::UIColor { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $strokeColor: Combine.Published.Publisher { + final public var $strokeColor: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($strokeWidth) final public var strokeWidth: Swift.Double { + @Combine::Published @_projectedValueProperty($strokeWidth) final public var strokeWidth: Swift::Double { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $strokeWidth: Combine.Published.Publisher { + final public var $strokeWidth: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($fillColor) final public var fillColor: UIKit.UIColor { + @Combine::Published @_projectedValueProperty($fillColor) final public var fillColor: UIKit::UIColor { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $fillColor: Combine.Published.Publisher { + final public var $fillColor: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($extra) final public var extra: Any? { + @Combine::Published @_projectedValueProperty($extra) final public var extra: Any? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $extra: Combine.Published.Publisher { + final public var $extra: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($zIndex) final public var zIndex: Swift.Int? { + @Combine::Published @_projectedValueProperty($zIndex) final public var zIndex: Swift::Int? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $zIndex: Combine.Published.Publisher { + final public var $zIndex: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore.OnCircleEventHandler? { + @Combine::Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore::OnCircleEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onClick: Combine.Published.Publisher { + final public var $onClick: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - public init(center: any MapConductorCore.GeoPointProtocol, radiusMeters: Swift.Double, geodesic: Swift.Bool = true, clickable: Swift.Bool = true, strokeColor: UIKit.UIColor = .red, strokeWidth: Swift.Double = 1.0, fillColor: UIKit.UIColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5), id: Swift.String? = nil, zIndex: Swift.Int? = nil, extra: Any? = nil, onClick: MapConductorCore.OnCircleEventHandler? = nil) - final public func copy(center: (any MapConductorCore.GeoPointProtocol)? = nil, radiusMeters: Swift.Double? = nil, geodesic: Swift.Bool? = nil, clickable: Swift.Bool? = nil, strokeColor: UIKit.UIColor? = nil, strokeWidth: Swift.Double? = nil, fillColor: UIKit.UIColor? = nil, id: Swift.String? = nil, zIndex: Swift.Int? = nil, extra: Any? = nil, onClick: MapConductorCore.OnCircleEventHandler? = nil) -> MapConductorCore.CircleState - final public func fingerPrint() -> MapConductorCore.CircleFingerPrint - final public func asFlow() -> Combine.AnyPublisher - public static func == (lhs: MapConductorCore.CircleState, rhs: MapConductorCore.CircleState) -> Swift.Bool - final public func hash(into hasher: inout Swift.Hasher) - final public func hashCode() -> Swift.Int - public typealias ID = Swift.String - public typealias ObjectWillChangePublisher = Combine.ObservableObjectPublisher + public init(center: any MapConductorCore::GeoPointProtocol, radiusMeters: Swift::Double, geodesic: Swift::Bool = true, clickable: Swift::Bool = true, strokeColor: UIKit::UIColor = .red, strokeWidth: Swift::Double = 1.0, fillColor: UIKit::UIColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5), id: Swift::String? = nil, zIndex: Swift::Int? = nil, extra: Any? = nil, onClick: MapConductorCore::OnCircleEventHandler? = nil) + final public func copy(center: (any MapConductorCore::GeoPointProtocol)? = nil, radiusMeters: Swift::Double? = nil, geodesic: Swift::Bool? = nil, clickable: Swift::Bool? = nil, strokeColor: UIKit::UIColor? = nil, strokeWidth: Swift::Double? = nil, fillColor: UIKit::UIColor? = nil, id: Swift::String? = nil, zIndex: Swift::Int? = nil, extra: Any? = nil, onClick: MapConductorCore::OnCircleEventHandler? = nil) -> MapConductorCore::CircleState + final public func fingerPrint() -> MapConductorCore::CircleFingerPrint + final public func asFlow() -> Combine::AnyPublisher + public static func == (lhs: MapConductorCore::CircleState, rhs: MapConductorCore::CircleState) -> Swift::Bool + final public func hash(into hasher: inout Swift::Hasher) + final public func hashCode() -> Swift::Int + public typealias ID = Swift::String + public typealias ObjectWillChangePublisher = Combine::ObservableObjectPublisher @objc deinit - final public var hashValue: Swift.Int { + final public var hashValue: Swift::Int { get } } public enum MapDriverConformance { - public struct ViolationError : Swift.Error, Swift.CustomStringConvertible { - public let description: Swift.String - public init(_ description: Swift.String) + public struct ViolationError : Swift::Error, Swift::CustomStringConvertible { + public let description: Swift::String + public init(_ description: Swift::String) } - public static func checkZoomConverter(_ converter: MapConductorCore.WebMercatorZoomAltitudeConverter, latitudes: [Swift.Double] = [0.0, 35.0, 60.0, 85.0, -85.0], zooms: [Swift.Double] = [0.0, 1.0, 5.5, 10.0, 15.25, 22.0]) throws - public static func checkOverlaySlots(_ controllers: [any MapConductorCore.AnyOverlayController], expected: Swift.Set = Set(OverlayKind.allCases)) throws - public static func checkCascadeOrder(_ order: [MapConductorCore.OverlayKind] = OverlayHitResolver.canonicalOrder) throws - public static func checkCapabilityDeclarations(_ registry: any MapConductorCore.MapServiceRegistry) throws - public static func checkProjectionRoundTrip(toScreen: (any MapConductorCore.GeoPointProtocol) -> CoreFoundation.CGPoint?, fromScreen: (CoreFoundation.CGPoint) -> (any MapConductorCore.GeoPointProtocol)?, samples: [any MapConductorCore.GeoPointProtocol]) throws + public static func checkZoomConverter(_ converter: MapConductorCore::WebMercatorZoomAltitudeConverter, latitudes: [Swift::Double] = [0.0, 35.0, 60.0, 85.0, -85.0], zooms: [Swift::Double] = [0.0, 1.0, 5.5, 10.0, 15.25, 22.0]) throws + public static func checkOverlaySlots(_ controllers: [any MapConductorCore::AnyOverlayController], expected: Swift::Set = Set(OverlayKind.allCases)) throws + public static func checkCascadeOrder(_ order: [MapConductorCore::OverlayKind] = OverlayHitResolver.canonicalOrder) throws + public static func checkCapabilityDeclarations(_ registry: any MapConductorCore::MapServiceRegistry) throws + public static func checkProjectionRoundTrip(toScreen: (any MapConductorCore::GeoPointProtocol) -> CoreFoundation::CGPoint?, fromScreen: (CoreFoundation::CGPoint) -> (any MapConductorCore::GeoPointProtocol)?, samples: [any MapConductorCore::GeoPointProtocol]) throws } -public typealias OnMapInitializedHandler = (MapConductorCore.InitState) -> Swift.Void +public typealias OnMapInitializedHandler = (MapConductorCore::InitState) -> Swift::Void public protocol MapViewControllerProtocol { - var holder: MapConductorCore.AnyMapViewHolder { get } - var coroutine: MapConductorCore.CoroutineScope { get } + var holder: MapConductorCore::AnyMapViewHolder { get } + var coroutine: MapConductorCore::CoroutineScope { get } func clearOverlays() async - func setCameraMoveStartListener(listener: MapConductorCore.OnCameraMoveHandler?) - func setCameraMoveListener(listener: MapConductorCore.OnCameraMoveHandler?) - func setCameraMoveEndListener(listener: MapConductorCore.OnCameraMoveHandler?) - func setMapClickListener(listener: MapConductorCore.OnMapEventHandler?) - func setMapLongClickListener(listener: MapConductorCore.OnMapEventHandler?) - func setMapInitializedListener(listener: MapConductorCore.OnMapInitializedHandler?) - func moveCamera(position: MapConductorCore.MapCameraPosition) - func animateCamera(position: MapConductorCore.MapCameraPosition, duration: MapConductorCore.Long) - func fitBounds(bounds: MapConductorCore.GeoRectBounds, padding: Swift.Int) - var overlayControllers: MapConductorCore.OverlayControllerRegistry { get } + func setCameraMoveStartListener(listener: MapConductorCore::OnCameraMoveHandler?) + func setCameraMoveListener(listener: MapConductorCore::OnCameraMoveHandler?) + func setCameraMoveEndListener(listener: MapConductorCore::OnCameraMoveHandler?) + func setMapClickListener(listener: MapConductorCore::OnMapEventHandler?) + func setMapLongClickListener(listener: MapConductorCore::OnMapEventHandler?) + func setMapInitializedListener(listener: MapConductorCore::OnMapInitializedHandler?) + func moveCamera(position: MapConductorCore::MapCameraPosition) + func animateCamera(position: MapConductorCore::MapCameraPosition, duration: MapConductorCore::Long) + func fitBounds(bounds: MapConductorCore::GeoRectBounds, padding: Swift::Int) + var overlayControllers: MapConductorCore::OverlayControllerRegistry { get } func destroy() - func applyUISettings(_ settings: MapConductorCore.MapUISettings) - func setCameraRestriction(_ restriction: MapConductorCore.CameraRestriction?) + func applyUISettings(_ settings: MapConductorCore::MapUISettings) + func setCameraRestriction(_ restriction: MapConductorCore::CameraRestriction?) } -extension MapConductorCore.MapViewControllerProtocol { - public func setCameraRestriction(_ restriction: MapConductorCore.CameraRestriction?) - public func registerOverlayController(_ controller: any MapConductorCore.AnyOverlayController) - public func unregisterOverlayController(_ controller: any MapConductorCore.AnyOverlayController) +extension MapConductorCore::MapViewControllerProtocol { + public func setCameraRestriction(_ restriction: MapConductorCore::CameraRestriction?) + public func registerOverlayController(_ controller: any MapConductorCore::AnyOverlayController) + public func unregisterOverlayController(_ controller: any MapConductorCore::AnyOverlayController) public func destroy() - public func applyUISettings(_ settings: MapConductorCore.MapUISettings) + public func applyUISettings(_ settings: MapConductorCore::MapUISettings) } -public protocol OverlayControllerProtocol : MapConductorCore.AnyOverlayController { +public protocol OverlayControllerProtocol : MapConductorCore::AnyOverlayController { associatedtype StateType associatedtype EntityType associatedtype EventType - var zIndex: Swift.Int { get } + var zIndex: Swift::Int { get } func add(data: [Self.StateType]) async func update(state: Self.StateType) async func clear() async - var clickListener: ((Self.EventType) -> Swift.Void)? { get set } - func find(position: any MapConductorCore.GeoPointProtocol) -> Self.EntityType? - func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + var clickListener: ((Self.EventType) -> Swift::Void)? { get set } + func find(position: any MapConductorCore::GeoPointProtocol) -> Self.EntityType? + func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async func destroy() } public protocol AnyOverlayController : AnyObject { - var zIndex: Swift.Int { get } - func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + var zIndex: Swift::Int { get } + func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async func destroy() } final public class OverlayControllerRegistry { public init() - final public func register(_ controller: any MapConductorCore.AnyOverlayController) - final public func unregister(_ controller: any MapConductorCore.AnyOverlayController) - final public func all() -> [any MapConductorCore.AnyOverlayController] - final public func dispatchCameraChanged(_ mapCameraPosition: MapConductorCore.MapCameraPosition) + final public func register(_ controller: any MapConductorCore::AnyOverlayController) + final public func unregister(_ controller: any MapConductorCore::AnyOverlayController) + final public func all() -> [any MapConductorCore::AnyOverlayController] + final public func dispatchCameraChanged(_ mapCameraPosition: MapConductorCore::MapCameraPosition) final public func destroyAll() @objc deinit } -public enum OverlayControllerRegistryKey : MapConductorCore.MapServiceKey { - public typealias Value = MapConductorCore.OverlayControllerRegistry +public enum OverlayControllerRegistryKey : MapConductorCore::MapServiceKey { + public typealias Value = MapConductorCore::OverlayControllerRegistry } final public class OverlayHit { - final public let kind: MapConductorCore.OverlayKind - final public let clicked: any MapConductorCore.GeoPointProtocol - public init(kind: MapConductorCore.OverlayKind, clicked: any MapConductorCore.GeoPointProtocol, deliver: @escaping () -> Swift.Void) + final public let kind: MapConductorCore::OverlayKind + final public let clicked: any MapConductorCore::GeoPointProtocol + public init(kind: MapConductorCore::OverlayKind, clicked: any MapConductorCore::GeoPointProtocol, deliver: @escaping () -> Swift::Void) final public func dispatch() @objc deinit } public enum OverlayHitResolver { - public static let canonicalOrder: [MapConductorCore.OverlayKind] - public static func firstHit(_ controllers: [any MapConductorCore.AnyOverlayController], order: [MapConductorCore.OverlayKind] = canonicalOrder, probe: (any MapConductorCore.SlottedOverlayController) -> T?) -> T? - public static func resolve(_ controllers: [any MapConductorCore.AnyOverlayController], position: any MapConductorCore.GeoPointProtocol, order: [MapConductorCore.OverlayKind] = canonicalOrder) -> MapConductorCore.OverlayHit? + public static let canonicalOrder: [MapConductorCore::OverlayKind] + public static func firstHit(_ controllers: [any MapConductorCore::AnyOverlayController], order: [MapConductorCore::OverlayKind] = canonicalOrder, probe: (any MapConductorCore::SlottedOverlayController) -> T?) -> T? + public static func resolve(_ controllers: [any MapConductorCore::AnyOverlayController], position: any MapConductorCore::GeoPointProtocol, order: [MapConductorCore::OverlayKind] = canonicalOrder) -> MapConductorCore::OverlayHit? } -extension MapConductorCore.OverlayControllerRegistry { +extension MapConductorCore::OverlayControllerRegistry { @discardableResult - final public func dispatchOverlayTap(position: any MapConductorCore.GeoPointProtocol, order: [MapConductorCore.OverlayKind] = OverlayHitResolver.canonicalOrder) -> Swift.Bool + final public func dispatchOverlayTap(position: any MapConductorCore::GeoPointProtocol, order: [MapConductorCore::OverlayKind] = OverlayHitResolver.canonicalOrder) -> Swift::Bool } -extension MapConductorCore.MapViewControllerProtocol { +extension MapConductorCore::MapViewControllerProtocol { @discardableResult - public func dispatchOverlayTap(position: any MapConductorCore.GeoPointProtocol) -> Swift.Bool + public func dispatchOverlayTap(position: any MapConductorCore::GeoPointProtocol) -> Swift::Bool } -public enum OverlayKind : Swift.String, Swift.CaseIterable, Swift.Sendable { +public enum OverlayKind : Swift::String, Swift::CaseIterable, Swift::Sendable { case marker case circle case groundImage case polyline case polygon case rasterLayer - public init?(rawValue: Swift.String) - public typealias AllCases = [MapConductorCore.OverlayKind] - public typealias RawValue = Swift.String - nonisolated public static var allCases: [MapConductorCore.OverlayKind] { + public init?(rawValue: Swift::String) + public typealias AllCases = [MapConductorCore::OverlayKind] + public typealias RawValue = Swift::String + nonisolated public static var allCases: [MapConductorCore::OverlayKind] { get } - public var rawValue: Swift.String { + public var rawValue: Swift::String { get } } -public protocol SlottedOverlayController : MapConductorCore.AnyOverlayController { - var kind: MapConductorCore.OverlayKind { get } - func hasId(_ id: Swift.String) -> Swift.Bool - func resolveTap(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.OverlayHit? +public protocol SlottedOverlayController : MapConductorCore::AnyOverlayController { + var kind: MapConductorCore::OverlayKind { get } + func hasId(_ id: Swift::String) -> Swift::Bool + func resolveTap(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::OverlayHit? } -extension MapConductorCore.OverlayControllerRegistry { - final public func slotted() -> [any MapConductorCore.SlottedOverlayController] - final public func primary(_ kind: MapConductorCore.OverlayKind) -> (any MapConductorCore.SlottedOverlayController)? - final public func controllers(of kind: MapConductorCore.OverlayKind) -> [any MapConductorCore.SlottedOverlayController] - final public func hasOverlay(_ kind: MapConductorCore.OverlayKind, id: Swift.String) -> Swift.Bool +extension MapConductorCore::OverlayControllerRegistry { + final public func slotted() -> [any MapConductorCore::SlottedOverlayController] + final public func primary(_ kind: MapConductorCore::OverlayKind) -> (any MapConductorCore::SlottedOverlayController)? + final public func controllers(of kind: MapConductorCore::OverlayKind) -> [any MapConductorCore::SlottedOverlayController] + final public func hasOverlay(_ kind: MapConductorCore::OverlayKind, id: Swift::String) -> Swift::Bool } public enum MCLog { - public static let isEnabled: Swift.Bool - public static func marker(_ message: Swift.String) - public static func map(_ message: Swift.String) + public static let isEnabled: Swift::Bool + public static func marker(_ message: Swift::String) + public static func map(_ message: Swift::String) + public static func tileServer(_ message: Swift::String) + public static func tileDetail(_ message: Swift::String) + public static func probe(_ message: Swift::String) } public protocol GeoPointProtocol { - func wrap() -> any MapConductorCore.GeoPointProtocol - var latitude: Swift.Double { get } - var longitude: Swift.Double { get } - var altitude: Swift.Double? { get } + func wrap() -> any MapConductorCore::GeoPointProtocol + var latitude: Swift::Double { get } + var longitude: Swift::Double { get } + var altitude: Swift::Double? { get } } -public struct GeoPoint : MapConductorCore.GeoPointProtocol, Swift.Equatable, Swift.Hashable { - public let latitude: Swift.Double - public let longitude: Swift.Double - public var altitude: Swift.Double? { +public struct GeoPoint : MapConductorCore::GeoPointProtocol, Swift::Equatable, Swift::Hashable { + public let latitude: Swift::Double + public let longitude: Swift::Double + public var altitude: Swift::Double? { get } - public init(latitude: Swift.Double, longitude: Swift.Double, altitude: Swift.Double = 0.0) - public func toUrlValue(precision: Swift.Int = 6) -> Swift.String - public static func == (lhs: MapConductorCore.GeoPoint, rhs: MapConductorCore.GeoPoint) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public func wrap() -> any MapConductorCore.GeoPointProtocol - public static func fromLatLong(latitude: Swift.Double, longitude: Swift.Double) -> MapConductorCore.GeoPoint - public static func fromLongLat(longitude: Swift.Double, latitude: Swift.Double) -> MapConductorCore.GeoPoint - public static func from(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.GeoPoint - public var hashValue: Swift.Int { + public init(latitude: Swift::Double, longitude: Swift::Double, altitude: Swift::Double = 0.0) + public func toUrlValue(precision: Swift::Int = 6) -> Swift::String + public static func == (lhs: MapConductorCore::GeoPoint, rhs: MapConductorCore::GeoPoint) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public func wrap() -> any MapConductorCore::GeoPointProtocol + public static func fromLatLong(latitude: Swift::Double, longitude: Swift::Double) -> MapConductorCore::GeoPoint + public static func fromLongLat(longitude: Swift::Double, latitude: Swift::Double) -> MapConductorCore::GeoPoint + public static func from(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::GeoPoint + public var hashValue: Swift::Int { get } } -extension MapConductorCore.GeoPointProtocol { - public func normalize() -> MapConductorCore.GeoPoint - public func isValid() -> Swift.Bool +extension MapConductorCore::GeoPointProtocol { + public func normalize() -> MapConductorCore::GeoPoint + public func isValid() -> Swift::Bool } -final public class GeoRectBounds : Swift.Equatable, Swift.Hashable { - public init(southWest: MapConductorCore.GeoPoint? = nil, northEast: MapConductorCore.GeoPoint? = nil) - final public var isEmpty: Swift.Bool { +final public class GeoRectBounds : Swift::Equatable, Swift::Hashable { + public init(southWest: MapConductorCore::GeoPoint? = nil, northEast: MapConductorCore::GeoPoint? = nil) + final public var isEmpty: Swift::Bool { get } - final public var southWest: MapConductorCore.GeoPoint? { + final public var southWest: MapConductorCore::GeoPoint? { get } - final public var northEast: MapConductorCore.GeoPoint? { + final public var northEast: MapConductorCore::GeoPoint? { get } - final public func extend(point: any MapConductorCore.GeoPointProtocol) - final public func contains(point: any MapConductorCore.GeoPointProtocol) -> Swift.Bool - final public var center: MapConductorCore.GeoPoint? { + final public func extend(point: any MapConductorCore::GeoPointProtocol) + final public func contains(point: any MapConductorCore::GeoPointProtocol) -> Swift::Bool + final public var center: MapConductorCore::GeoPoint? { get } - final public func union(other: MapConductorCore.GeoRectBounds) -> MapConductorCore.GeoRectBounds - final public func toSpan() -> MapConductorCore.GeoPoint? - final public func toUrlValue(precision: Swift.Int = 6) -> Swift.String - final public func expandedByDegrees(latPad: Swift.Double, lonPad: Swift.Double) -> MapConductorCore.GeoRectBounds - final public func intersects(other: MapConductorCore.GeoRectBounds) -> Swift.Bool - final public func equals(other: MapConductorCore.GeoRectBounds) -> Swift.Bool - final public func toString() -> Swift.String - public static func == (lhs: MapConductorCore.GeoRectBounds, rhs: MapConductorCore.GeoRectBounds) -> Swift.Bool - final public func hash(into hasher: inout Swift.Hasher) + final public func union(other: MapConductorCore::GeoRectBounds) -> MapConductorCore::GeoRectBounds + final public func toSpan() -> MapConductorCore::GeoPoint? + final public func toUrlValue(precision: Swift::Int = 6) -> Swift::String + final public func expandedByDegrees(latPad: Swift::Double, lonPad: Swift::Double) -> MapConductorCore::GeoRectBounds + final public func intersects(other: MapConductorCore::GeoRectBounds) -> Swift::Bool + final public func equals(other: MapConductorCore::GeoRectBounds) -> Swift::Bool + final public func toString() -> Swift::String + public static func == (lhs: MapConductorCore::GeoRectBounds, rhs: MapConductorCore::GeoRectBounds) -> Swift::Bool + final public func hash(into hasher: inout Swift::Hasher) @objc deinit - final public var hashValue: Swift.Int { + final public var hashValue: Swift::Int { get } } public enum Earth { - public static let radiusMeters: Swift.Double - public static let circumferenceMeters: Swift.Double - public static let flattening: Swift.Double - public static let semiMinorAxisMeters: Swift.Double - public static let eccentricitySquared: Swift.Double + public static let radiusMeters: Swift::Double + public static let circumferenceMeters: Swift::Double + public static let flattening: Swift::Double + public static let semiMinorAxisMeters: Swift::Double + public static let eccentricitySquared: Swift::Double } final public class HexCellRegistry { - public init(geocell: any MapConductorCore.HexGeocellProtocol, zoom: Swift.Double) - final public func getCell(entity: MapConductorCore.MarkerEntity) -> MapConductorCore.HexCell + public init(geocell: any MapConductorCore::HexGeocellProtocol, zoom: Swift::Double) + final public func getCell(entity: MapConductorCore::MarkerEntity) -> MapConductorCore::HexCell @discardableResult - final public func setPoint(entity: MapConductorCore.MarkerEntity) -> MapConductorCore.HexCell - final public func contains(hexId: Swift.String) -> Swift.Bool + final public func setPoint(entity: MapConductorCore::MarkerEntity) -> MapConductorCore::HexCell + final public func contains(hexId: Swift::String) -> Swift::Bool @discardableResult - final public func removePoint(entity: MapConductorCore.MarkerEntity) -> Swift.Bool + final public func removePoint(entity: MapConductorCore::MarkerEntity) -> Swift::Bool final public func clear() - final public func findNearest(point: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.HexCell? - final public func findNearestWithDistance(point: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.HexCellWithDistance? - final public func findNearestKWithDistance(point: any MapConductorCore.GeoPointProtocol, k: Swift.Int) -> [MapConductorCore.HexCellWithDistance] - final public func findWithinRadiusWithDistance(point: any MapConductorCore.GeoPointProtocol, radius: Swift.Double) -> [MapConductorCore.HexCellWithDistance] - final public func all() -> [MapConductorCore.HexCell] - final public func getEntryIDsByHexCell(_ hexCell: MapConductorCore.HexCell) -> Swift.Set? - final public func metersPerPixel(position: any MapConductorCore.GeoPointProtocol, zoom: Swift.Double, pixels: Swift.Double, tileSize: Swift.Int = 256) -> Swift.Double - final public func findWithinPixelRadius(position: any MapConductorCore.GeoPointProtocol, zoom: Swift.Double, pixels: Swift.Double, tileSize: Swift.Int = 256) -> [MapConductorCore.HexCellWithDistance] - final public func findByIdPrefix(_ prefix: Swift.String) -> [MapConductorCore.HexCell] - final public func getStats() -> MapConductorCore.RegistryStats + final public func findNearest(point: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::HexCell? + final public func findNearestWithDistance(point: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::HexCellWithDistance? + final public func findNearestKWithDistance(point: any MapConductorCore::GeoPointProtocol, k: Swift::Int) -> [MapConductorCore::HexCellWithDistance] + final public func findWithinRadiusWithDistance(point: any MapConductorCore::GeoPointProtocol, radius: Swift::Double) -> [MapConductorCore::HexCellWithDistance] + final public func findWithinRadius(point: any MapConductorCore::GeoPointProtocol, radius: Swift::Double) -> [MapConductorCore::HexCell] + final public func all() -> [MapConductorCore::HexCell] + final public func getEntryIDsByHexCell(_ hexCell: MapConductorCore::HexCell) -> Swift::Set? + final public func metersPerPixel(position: any MapConductorCore::GeoPointProtocol, zoom: Swift::Double, pixels: Swift::Double, tileSize: Swift::Int = 256) -> Swift::Double + final public func findWithinPixelRadius(position: any MapConductorCore::GeoPointProtocol, zoom: Swift::Double, pixels: Swift::Double, tileSize: Swift::Int = 256) -> [MapConductorCore::HexCellWithDistance] + final public func findByIdPrefix(_ prefix: Swift::String) -> [MapConductorCore::HexCell] + final public func getStats() -> MapConductorCore::RegistryStats @objc deinit } -public struct RegistryStats : Swift.Sendable, Swift.Hashable { - public let totalCells: Swift.Int - public let totalEntries: Swift.Int - public let kdTreeBuilt: Swift.Bool - public let needsRebuild: Swift.Bool - public static func == (a: MapConductorCore.RegistryStats, b: MapConductorCore.RegistryStats) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public struct RegistryStats : Swift::Sendable, Swift::Hashable { + public let totalCells: Swift::Int + public let totalEntries: Swift::Int + public let kdTreeBuilt: Swift::Bool + public let needsRebuild: Swift::Bool + public static func == (a: MapConductorCore::RegistryStats, b: MapConductorCore::RegistryStats) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } -public struct HexCoord : Swift.Sendable, Swift.Hashable, Swift.CustomStringConvertible { - public let q: Swift.Int - public let r: Swift.Int - public let depth: Swift.Int - public init(q: Swift.Int, r: Swift.Int, depth: Swift.Int = 0) - public var description: Swift.String { +public struct HexCoord : Swift::Sendable, Swift::Hashable, Swift::CustomStringConvertible { + public let q: Swift::Int + public let r: Swift::Int + public let depth: Swift::Int + public init(q: Swift::Int, r: Swift::Int, depth: Swift::Int = 0) + public var description: Swift::String { get } - public var s: Swift.Int { + public var s: Swift::Int { get } - public func neighbors() -> [MapConductorCore.HexCoord] - public static func == (a: MapConductorCore.HexCoord, b: MapConductorCore.HexCoord) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { + public func neighbors() -> [MapConductorCore::HexCoord] + public static func == (a: MapConductorCore::HexCoord, b: MapConductorCore::HexCoord) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } -public enum Direction6 : Swift.CaseIterable, Swift.Sendable { +public enum Direction6 : Swift::CaseIterable, Swift::Sendable { case right case rightUp case leftUp case left case leftDown case rightDown - public static func == (a: MapConductorCore.Direction6, b: MapConductorCore.Direction6) -> Swift.Bool - public typealias AllCases = [MapConductorCore.Direction6] - nonisolated public static var allCases: [MapConductorCore.Direction6] { - get - } - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -public struct HexCell : Swift.Sendable, Swift.Hashable { - public let coord: MapConductorCore.HexCoord - public let centerLatLng: MapConductorCore.GeoPoint - public let centerXY: CoreFoundation.CGPoint - public let id: Swift.String - public init(coord: MapConductorCore.HexCoord, centerLatLng: MapConductorCore.GeoPoint, centerXY: CoreFoundation.CGPoint, id: Swift.String) - public func idPrefix(levels: Swift.Int) -> Swift.String - public static func == (lhs: MapConductorCore.HexCell, rhs: MapConductorCore.HexCell) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -public struct HexCellWithDistance : Swift.Sendable, Swift.Hashable { - public let cell: MapConductorCore.HexCell - public let distanceMeters: Swift.Double - public static func == (a: MapConductorCore.HexCellWithDistance, b: MapConductorCore.HexCellWithDistance) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -public struct IdentifiedHexCell : Swift.Sendable, Swift.Hashable { - public let id: Swift.String - public let cell: MapConductorCore.HexCell - public static func == (a: MapConductorCore.IdentifiedHexCell, b: MapConductorCore.IdentifiedHexCell) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -public protocol HexGeocellProtocol : Swift.Sendable { - var projection: any MapConductorCore.ProjectionProtocol { get } - var baseHexSideLength: Swift.Int { get } - func latLngToHexCoord(position: any MapConductorCore.GeoPointProtocol, zoom: Swift.Double) -> MapConductorCore.HexCoord - func latLngToHexCell(position: any MapConductorCore.GeoPointProtocol, zoom: Swift.Double) -> MapConductorCore.HexCell - func hexToLatLngCenter(coord: MapConductorCore.HexCoord, latHint: Swift.Double, zoom: Swift.Double) -> MapConductorCore.GeoPoint - func hexToCellId(coord: MapConductorCore.HexCoord, zoom: Swift.Double) -> Swift.String - func hexToPolygonLatLng(coord: MapConductorCore.HexCoord, latHint: Swift.Double, zoom: Swift.Double) -> [MapConductorCore.GeoPoint] - func enclosingCell(of points: [MapConductorCore.MarkerState], zoom: Swift.Double) -> MapConductorCore.HexCell - func hexCellsForPointsWithId(points: [MapConductorCore.MarkerState], zoom: Swift.Double) -> Swift.Set - func hexDistance(a: MapConductorCore.HexCoord, b: MapConductorCore.HexCoord) -> Swift.Int - func hexRange(center: MapConductorCore.HexCoord, radius: Swift.Int) -> [MapConductorCore.HexCoord] -} -public struct HexGeocell : MapConductorCore.HexGeocellProtocol { - public let projection: any MapConductorCore.ProjectionProtocol - public let baseHexSideLength: Swift.Int - public init(projection: any MapConductorCore.ProjectionProtocol, baseHexSideLength: Swift.Int = 1_000) - public func latLngToHexCoord(position: any MapConductorCore.GeoPointProtocol, zoom: Swift.Double) -> MapConductorCore.HexCoord - public func latLngToHexCell(position: any MapConductorCore.GeoPointProtocol, zoom: Swift.Double) -> MapConductorCore.HexCell - public func hexToLatLngCenter(coord: MapConductorCore.HexCoord, latHint: Swift.Double, zoom: Swift.Double) -> MapConductorCore.GeoPoint - public func hexToCellId(coord: MapConductorCore.HexCoord, zoom: Swift.Double) -> Swift.String - public func hexToPolygonLatLng(coord: MapConductorCore.HexCoord, latHint: Swift.Double, zoom: Swift.Double) -> [MapConductorCore.GeoPoint] - public func enclosingCell(of points: [MapConductorCore.MarkerState], zoom: Swift.Double) -> MapConductorCore.HexCell - public func hexCellsForPointsWithId(points: [MapConductorCore.MarkerState], zoom: Swift.Double) -> Swift.Set - public func hexDistance(a: MapConductorCore.HexCoord, b: MapConductorCore.HexCoord) -> Swift.Int - public func hexRange(center: MapConductorCore.HexCoord, radius: Swift.Int) -> [MapConductorCore.HexCoord] - public static func defaultGeocell() -> any MapConductorCore.HexGeocellProtocol -} -final public class KDTree : Swift.Sendable { - public init(points: [MapConductorCore.HexCell]) - final public func nearest(query: CoreFoundation.CGPoint) -> MapConductorCore.HexCell? - final public func nearestWithDistance(query: CoreFoundation.CGPoint) -> MapConductorCore.HexCellWithDistance? - final public func nearestKWithDistance(query: CoreFoundation.CGPoint, k: Swift.Int) -> [MapConductorCore.HexCellWithDistance] - final public func withinRadiusWithDistance(query: CoreFoundation.CGPoint, radius: Swift.Double) -> [MapConductorCore.HexCellWithDistance] - @objc deinit -} -public protocol ProjectionProtocol : Swift.Sendable { - func project(_ point: any MapConductorCore.GeoPointProtocol) -> CoreFoundation.CGPoint - func unproject(_ point: CoreFoundation.CGPoint) -> MapConductorCore.GeoPoint -} -public struct WebMercatorProjection : MapConductorCore.ProjectionProtocol { + public static func == (a: MapConductorCore::Direction6, b: MapConductorCore::Direction6) -> Swift::Bool + public typealias AllCases = [MapConductorCore::Direction6] + nonisolated public static var allCases: [MapConductorCore::Direction6] { + get + } + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { + get + } +} +public struct HexCell : Swift::Sendable, Swift::Hashable { + public let coord: MapConductorCore::HexCoord + public let centerLatLng: MapConductorCore::GeoPoint + public let centerXY: CoreFoundation::CGPoint + public let id: Swift::String + public init(coord: MapConductorCore::HexCoord, centerLatLng: MapConductorCore::GeoPoint, centerXY: CoreFoundation::CGPoint, id: Swift::String) + public func idPrefix(levels: Swift::Int) -> Swift::String + public static func == (lhs: MapConductorCore::HexCell, rhs: MapConductorCore::HexCell) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { + get + } +} +public struct HexCellWithDistance : Swift::Sendable, Swift::Hashable { + public let cell: MapConductorCore::HexCell + public let distanceMeters: Swift::Double + public static func == (a: MapConductorCore::HexCellWithDistance, b: MapConductorCore::HexCellWithDistance) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { + get + } +} +public struct IdentifiedHexCell : Swift::Sendable, Swift::Hashable { + public let id: Swift::String + public let cell: MapConductorCore::HexCell + public static func == (a: MapConductorCore::IdentifiedHexCell, b: MapConductorCore::IdentifiedHexCell) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { + get + } +} +public protocol HexGeocellProtocol : Swift::Sendable { + var projection: any MapConductorCore::ProjectionProtocol { get } + var baseHexSideLength: Swift::Int { get } + func latLngToHexCoord(position: any MapConductorCore::GeoPointProtocol, zoom: Swift::Double) -> MapConductorCore::HexCoord + func latLngToHexCell(position: any MapConductorCore::GeoPointProtocol, zoom: Swift::Double) -> MapConductorCore::HexCell + func hexToLatLngCenter(coord: MapConductorCore::HexCoord, latHint: Swift::Double, zoom: Swift::Double) -> MapConductorCore::GeoPoint + func hexToCellId(coord: MapConductorCore::HexCoord, zoom: Swift::Double) -> Swift::String + func hexToPolygonLatLng(coord: MapConductorCore::HexCoord, latHint: Swift::Double, zoom: Swift::Double) -> [MapConductorCore::GeoPoint] + func enclosingCell(of points: [MapConductorCore::MarkerState], zoom: Swift::Double) -> MapConductorCore::HexCell + func hexCellsForPointsWithId(points: [MapConductorCore::MarkerState], zoom: Swift::Double) -> Swift::Set + func hexDistance(a: MapConductorCore::HexCoord, b: MapConductorCore::HexCoord) -> Swift::Int + func hexRange(center: MapConductorCore::HexCoord, radius: Swift::Int) -> [MapConductorCore::HexCoord] +} +public struct HexGeocell : MapConductorCore::HexGeocellProtocol { + public let projection: any MapConductorCore::ProjectionProtocol + public let baseHexSideLength: Swift::Int + public init(projection: any MapConductorCore::ProjectionProtocol, baseHexSideLength: Swift::Int = 1_000) + public func latLngToHexCoord(position: any MapConductorCore::GeoPointProtocol, zoom: Swift::Double) -> MapConductorCore::HexCoord + public func latLngToHexCell(position: any MapConductorCore::GeoPointProtocol, zoom: Swift::Double) -> MapConductorCore::HexCell + public func hexToLatLngCenter(coord: MapConductorCore::HexCoord, latHint: Swift::Double, zoom: Swift::Double) -> MapConductorCore::GeoPoint + public func hexToCellId(coord: MapConductorCore::HexCoord, zoom: Swift::Double) -> Swift::String + public func hexToPolygonLatLng(coord: MapConductorCore::HexCoord, latHint: Swift::Double, zoom: Swift::Double) -> [MapConductorCore::GeoPoint] + public func enclosingCell(of points: [MapConductorCore::MarkerState], zoom: Swift::Double) -> MapConductorCore::HexCell + public func hexCellsForPointsWithId(points: [MapConductorCore::MarkerState], zoom: Swift::Double) -> Swift::Set + public func hexDistance(a: MapConductorCore::HexCoord, b: MapConductorCore::HexCoord) -> Swift::Int + public func hexRange(center: MapConductorCore::HexCoord, radius: Swift::Int) -> [MapConductorCore::HexCoord] + public static func defaultGeocell() -> any MapConductorCore::HexGeocellProtocol +} +final public class KDTree : Swift::Sendable { + public init(points: [MapConductorCore::HexCell]) + final public func nearest(query: CoreFoundation::CGPoint) -> MapConductorCore::HexCell? + final public func nearestWithDistance(query: CoreFoundation::CGPoint) -> MapConductorCore::HexCellWithDistance? + final public func nearestKWithDistance(query: CoreFoundation::CGPoint, k: Swift::Int) -> [MapConductorCore::HexCellWithDistance] + final public func withinRadiusWithDistance(query: CoreFoundation::CGPoint, radius: Swift::Double) -> [MapConductorCore::HexCellWithDistance] + final public func withinRadius(query: CoreFoundation::CGPoint, radius: Swift::Double) -> [MapConductorCore::HexCell] + @objc deinit +} +public protocol ProjectionProtocol : Swift::Sendable { + func project(_ point: any MapConductorCore::GeoPointProtocol) -> CoreFoundation::CGPoint + func unproject(_ point: CoreFoundation::CGPoint) -> MapConductorCore::GeoPoint +} +public struct WebMercatorProjection : MapConductorCore::ProjectionProtocol { public init() - public func project(_ point: any MapConductorCore.GeoPointProtocol) -> CoreFoundation.CGPoint - public func unproject(_ point: CoreFoundation.CGPoint) -> MapConductorCore.GeoPoint + public func project(_ point: any MapConductorCore::GeoPointProtocol) -> CoreFoundation::CGPoint + public func unproject(_ point: CoreFoundation::CGPoint) -> MapConductorCore::GeoPoint } public enum WebMercatorScreenProjection { - public static func toScreenOffset(_ position: any MapConductorCore.GeoPointProtocol, camera: MapConductorCore.MapCameraPosition, size: CoreFoundation.CGSize) -> CoreFoundation.CGPoint? - public static func fromScreenOffset(_ offset: CoreFoundation.CGPoint, camera: MapConductorCore.MapCameraPosition, size: CoreFoundation.CGSize) -> MapConductorCore.GeoPoint? + public static func toScreenOffset(_ position: any MapConductorCore::GeoPointProtocol, camera: MapConductorCore::MapCameraPosition, size: CoreFoundation::CGSize) -> CoreFoundation::CGPoint? + public static func fromScreenOffset(_ offset: CoreFoundation::CGPoint, camera: MapConductorCore::MapCameraPosition, size: CoreFoundation::CGSize) -> MapConductorCore::GeoPoint? } -public let defaultCircleSegments: Swift.Int -public func circleToRing(center: any MapConductorCore.GeoPointProtocol, radiusMeters: Swift.Double, geodesic: Swift.Bool, segments: Swift.Int = defaultCircleSegments) -> [any MapConductorCore.GeoPointProtocol] -public func closeRing(_ ring: [any MapConductorCore.GeoPointProtocol]) -> [any MapConductorCore.GeoPointProtocol] +public let defaultCircleSegments: Swift::Int +public func circleToRing(center: any MapConductorCore::GeoPointProtocol, radiusMeters: Swift::Double, geodesic: Swift::Bool, segments: Swift::Int = defaultCircleSegments) -> [any MapConductorCore::GeoPointProtocol] +public func closeRing(_ ring: [any MapConductorCore::GeoPointProtocol]) -> [any MapConductorCore::GeoPointProtocol] public enum OverlayGeoJson { - public static func multiLineStringFeature(_ segments: [[any MapConductorCore.GeoPointProtocol]]) -> Swift.String? - public static func polygonFeature(_ rings: MapConductorCore.PolygonRings) -> Swift.String? - public static func ringsFeature(_ rings: [[any MapConductorCore.GeoPointProtocol]]) -> Swift.String? + public static func multiLineStringFeature(_ segments: [[any MapConductorCore::GeoPointProtocol]]) -> Swift::String? + public static func polygonFeature(_ rings: MapConductorCore::PolygonRings) -> Swift::String? + public static func ringsFeature(_ rings: [[any MapConductorCore::GeoPointProtocol]]) -> Swift::String? } public struct PolygonRings { - public let outerRings: [[any MapConductorCore.GeoPointProtocol]] - public let holeRings: [[any MapConductorCore.GeoPointProtocol]] - public init(outerRings: [[any MapConductorCore.GeoPointProtocol]], holeRings: [[any MapConductorCore.GeoPointProtocol]]) -} -public func densifyAndNormalize(_ points: [any MapConductorCore.GeoPointProtocol], geodesic: Swift.Bool, maxSegmentLength: Swift.Double = 10_000.0) -> [any MapConductorCore.GeoPointProtocol] -public func buildPolylineSegments(_ points: [any MapConductorCore.GeoPointProtocol], geodesic: Swift.Bool) -> [[any MapConductorCore.GeoPointProtocol]] -public func buildPolygonRings(points: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]], geodesic: Swift.Bool) -> MapConductorCore.PolygonRings -public func buildUnwrappedPolylinePath(_ points: [any MapConductorCore.GeoPointProtocol], geodesic: Swift.Bool, maxSegmentLength: Swift.Double = 10_000.0) -> [any MapConductorCore.GeoPointProtocol] -public func buildUnwrappedPolygonRings(points: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]], geodesic: Swift.Bool, maxSegmentLength: Swift.Double = 10_000.0) -> MapConductorCore.PolygonRings -public func splitRingByMeridian(_ ring: [any MapConductorCore.GeoPointProtocol], geodesic: Swift.Bool) -> [[any MapConductorCore.GeoPointProtocol]] -open class GroundImageController : MapConductorCore.OverlayControllerProtocol, MapConductorCore.SlottedOverlayController where ActualGroundImage == Renderer.ActualGroundImage, Renderer : MapConductorCore.GroundImageOverlayRendererProtocol { - public typealias StateType = MapConductorCore.GroundImageState - public typealias EntityType = MapConductorCore.GroundImageEntity - public typealias EventType = MapConductorCore.GroundImageEvent - final public let groundImageManager: MapConductorCore.GroundImageManager + public let outerRings: [[any MapConductorCore::GeoPointProtocol]] + public let holeRings: [[any MapConductorCore::GeoPointProtocol]] + public init(outerRings: [[any MapConductorCore::GeoPointProtocol]], holeRings: [[any MapConductorCore::GeoPointProtocol]]) +} +public func densifyAndNormalize(_ points: [any MapConductorCore::GeoPointProtocol], geodesic: Swift::Bool, maxSegmentLength: Swift::Double = 10_000.0) -> [any MapConductorCore::GeoPointProtocol] +public func buildPolylineSegments(_ points: [any MapConductorCore::GeoPointProtocol], geodesic: Swift::Bool) -> [[any MapConductorCore::GeoPointProtocol]] +public func buildPolygonRings(points: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]], geodesic: Swift::Bool) -> MapConductorCore::PolygonRings +public func buildUnwrappedPolylinePath(_ points: [any MapConductorCore::GeoPointProtocol], geodesic: Swift::Bool, maxSegmentLength: Swift::Double = 10_000.0) -> [any MapConductorCore::GeoPointProtocol] +public func buildUnwrappedPolygonRings(points: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]], geodesic: Swift::Bool, maxSegmentLength: Swift::Double = 10_000.0) -> MapConductorCore::PolygonRings +public func splitRingByMeridian(_ ring: [any MapConductorCore::GeoPointProtocol], geodesic: Swift::Bool) -> [[any MapConductorCore::GeoPointProtocol]] +open class GroundImageController : MapConductorCore::OverlayControllerProtocol, MapConductorCore::SlottedOverlayController where ActualGroundImage == Renderer.ActualGroundImage, Renderer : MapConductorCore::GroundImageOverlayRendererProtocol { + public typealias StateType = MapConductorCore::GroundImageState + public typealias EntityType = MapConductorCore::GroundImageEntity + public typealias EventType = MapConductorCore::GroundImageEvent + final public let groundImageManager: MapConductorCore::GroundImageManager open var renderer: Renderer - final public let zIndex: Swift.Int - public var clickListener: ((MapConductorCore.GroundImageEvent) -> Swift.Void)? - public init(groundImageManager: MapConductorCore.GroundImageManager, renderer: Renderer, clickListener: ((MapConductorCore.GroundImageEvent) -> Swift.Void)? = nil) - public func dispatchClick(event: MapConductorCore.GroundImageEvent) - open func add(data: [MapConductorCore.GroundImageState]) async - open func update(state: MapConductorCore.GroundImageState) async + final public let zIndex: Swift::Int + public var clickListener: ((MapConductorCore::GroundImageEvent) -> Swift::Void)? + public init(groundImageManager: MapConductorCore::GroundImageManager, renderer: Renderer, clickListener: ((MapConductorCore::GroundImageEvent) -> Swift::Void)? = nil) + public func dispatchClick(event: MapConductorCore::GroundImageEvent) + open func add(data: [MapConductorCore::GroundImageState]) async + open func update(state: MapConductorCore::GroundImageState) async open func clear() async - open func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.GroundImageEntity? - open func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + open func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::GroundImageEntity? + open func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async open func destroy() - public var kind: MapConductorCore.OverlayKind { + public var kind: MapConductorCore::OverlayKind { get } - public func hasId(_ id: Swift.String) -> Swift.Bool - public func resolveTap(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.OverlayHit? + public func hasId(_ id: Swift::String) -> Swift::Bool + public func resolveTap(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::OverlayHit? @objc deinit } public protocol GroundImageEntityProtocol { associatedtype ActualGroundImage var groundImage: Self.ActualGroundImage? { get set } - var state: MapConductorCore.GroundImageState { get } - var fingerPrint: MapConductorCore.GroundImageFingerPrint { get } + var state: MapConductorCore::GroundImageState { get } + var fingerPrint: MapConductorCore::GroundImageFingerPrint { get } } -final public class GroundImageEntity : MapConductorCore.GroundImageEntityProtocol { +final public class GroundImageEntity : MapConductorCore::GroundImageEntityProtocol { final public var groundImage: ActualGroundImage? - final public let state: MapConductorCore.GroundImageState - final public let fingerPrint: MapConductorCore.GroundImageFingerPrint - public init(groundImage: ActualGroundImage?, state: MapConductorCore.GroundImageState) + final public let state: MapConductorCore::GroundImageState + final public let fingerPrint: MapConductorCore::GroundImageFingerPrint + public init(groundImage: ActualGroundImage?, state: MapConductorCore::GroundImageState) @objc deinit } public protocol GroundImageManagerProtocol { associatedtype ActualGroundImage - func registerEntity(_ entity: MapConductorCore.GroundImageEntity) - func removeEntity(_ id: Swift.String) -> MapConductorCore.GroundImageEntity? - func getEntity(_ id: Swift.String) -> MapConductorCore.GroundImageEntity? - func hasEntity(_ id: Swift.String) -> Swift.Bool - func allEntities() -> [MapConductorCore.GroundImageEntity] + func registerEntity(_ entity: MapConductorCore::GroundImageEntity) + func removeEntity(_ id: Swift::String) -> MapConductorCore::GroundImageEntity? + func getEntity(_ id: Swift::String) -> MapConductorCore::GroundImageEntity? + func hasEntity(_ id: Swift::String) -> Swift::Bool + func allEntities() -> [MapConductorCore::GroundImageEntity] func clear() - func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.GroundImageEntity? + func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::GroundImageEntity? } -final public class GroundImageManager : MapConductorCore.GroundImageManagerProtocol { +final public class GroundImageManager : MapConductorCore::GroundImageManagerProtocol { public init() - final public func registerEntity(_ entity: MapConductorCore.GroundImageEntity) - final public func removeEntity(_ id: Swift.String) -> MapConductorCore.GroundImageEntity? - final public func getEntity(_ id: Swift.String) -> MapConductorCore.GroundImageEntity? - final public func hasEntity(_ id: Swift.String) -> Swift.Bool - final public func allEntities() -> [MapConductorCore.GroundImageEntity] + final public func registerEntity(_ entity: MapConductorCore::GroundImageEntity) + final public func removeEntity(_ id: Swift::String) -> MapConductorCore::GroundImageEntity? + final public func getEntity(_ id: Swift::String) -> MapConductorCore::GroundImageEntity? + final public func hasEntity(_ id: Swift::String) -> Swift::Bool + final public func allEntities() -> [MapConductorCore::GroundImageEntity] final public func clear() final public func destroy() - final public var isDestroyed: Swift.Bool { + final public var isDestroyed: Swift::Bool { get } - final public func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.GroundImageEntity? + final public func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::GroundImageEntity? @objc deinit } public protocol GroundImageOverlayRendererProtocol { associatedtype ActualGroundImage - func onAdd(data: [MapConductorCore.GroundImageOverlayAddParams]) async -> [Self.ActualGroundImage?] - func onChange(data: [MapConductorCore.GroundImageOverlayChangeParams]) async -> [Self.ActualGroundImage?] - func onRemove(data: [MapConductorCore.GroundImageEntity]) async + func onAdd(data: [MapConductorCore::GroundImageOverlayAddParams]) async -> [Self.ActualGroundImage?] + func onChange(data: [MapConductorCore::GroundImageOverlayChangeParams]) async -> [Self.ActualGroundImage?] + func onRemove(data: [MapConductorCore::GroundImageEntity]) async func onPostProcess() async } -open class AbstractGroundImageOverlayRenderer : MapConductorCore.GroundImageOverlayRendererProtocol { +open class AbstractGroundImageOverlayRenderer : MapConductorCore::GroundImageOverlayRendererProtocol { public init() open func onPostProcess() async - open func createGroundImage(state: MapConductorCore.GroundImageState) async -> ActualGroundImage? - open func updateGroundImageProperties(groundImage: ActualGroundImage, current: MapConductorCore.GroundImageEntity, prev: MapConductorCore.GroundImageEntity) async -> ActualGroundImage? - open func removeGroundImage(entity: MapConductorCore.GroundImageEntity) async - public func onAdd(data: [MapConductorCore.GroundImageOverlayAddParams]) async -> [ActualGroundImage?] - public func onChange(data: [MapConductorCore.GroundImageOverlayChangeParams]) async -> [ActualGroundImage?] - public func onRemove(data: [MapConductorCore.GroundImageEntity]) async + open func createGroundImage(state: MapConductorCore::GroundImageState) async -> ActualGroundImage? + open func updateGroundImageProperties(groundImage: ActualGroundImage, current: MapConductorCore::GroundImageEntity, prev: MapConductorCore::GroundImageEntity) async -> ActualGroundImage? + open func removeGroundImage(entity: MapConductorCore::GroundImageEntity) async + public func onAdd(data: [MapConductorCore::GroundImageOverlayAddParams]) async -> [ActualGroundImage?] + public func onChange(data: [MapConductorCore::GroundImageOverlayChangeParams]) async -> [ActualGroundImage?] + public func onRemove(data: [MapConductorCore::GroundImageEntity]) async @objc deinit } public struct GroundImageOverlayAddParams { - public let state: MapConductorCore.GroundImageState - public init(state: MapConductorCore.GroundImageState) + public let state: MapConductorCore::GroundImageState + public init(state: MapConductorCore::GroundImageState) } public struct GroundImageOverlayChangeParams { - public let current: MapConductorCore.GroundImageEntity - public let prev: MapConductorCore.GroundImageEntity - public init(current: MapConductorCore.GroundImageEntity, prev: MapConductorCore.GroundImageEntity) + public let current: MapConductorCore::GroundImageEntity + public let prev: MapConductorCore::GroundImageEntity + public init(current: MapConductorCore::GroundImageEntity, prev: MapConductorCore::GroundImageEntity) } -public struct GroundImageFingerPrint : Swift.Equatable, Swift.Hashable { - public let id: Swift.Int - public let bounds: Swift.Int - public let image: Swift.Int - public let opacity: Swift.Int - public let tileSize: Swift.Int - public let extra: Swift.Int - public static func == (a: MapConductorCore.GroundImageFingerPrint, b: MapConductorCore.GroundImageFingerPrint) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public struct GroundImageFingerPrint : Swift::Equatable, Swift::Hashable { + public let id: Swift::Int + public let bounds: Swift::Int + public let image: Swift::Int + public let opacity: Swift::Int + public let tileSize: Swift::Int + public let extra: Swift::Int + public static func == (a: MapConductorCore::GroundImageFingerPrint, b: MapConductorCore::GroundImageFingerPrint) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public struct GroundImageEvent { - public let state: MapConductorCore.GroundImageState - public let clicked: (any MapConductorCore.GeoPointProtocol)? - public init(state: MapConductorCore.GroundImageState, clicked: (any MapConductorCore.GeoPointProtocol)?) + public let state: MapConductorCore::GroundImageState + public let clicked: (any MapConductorCore::GeoPointProtocol)? + public init(state: MapConductorCore::GroundImageState, clicked: (any MapConductorCore::GeoPointProtocol)?) } -public typealias OnGroundImageEventHandler = (MapConductorCore.GroundImageEvent) -> Swift.Void -final public class GroundImageState : Combine.ObservableObject, Swift.Identifiable, Swift.Equatable, Swift.Hashable { - final public let id: Swift.String - @Combine.Published @_projectedValueProperty($bounds) final public var bounds: MapConductorCore.GeoRectBounds { +public typealias OnGroundImageEventHandler = (MapConductorCore::GroundImageEvent) -> Swift::Void +final public class GroundImageState : Combine::ObservableObject, Swift::Identifiable, Swift::Equatable, Swift::Hashable { + final public let id: Swift::String + final public let mutations: MapConductorCore::StateMutationSignal + @Combine::Published @_projectedValueProperty($bounds) final public var bounds: MapConductorCore::GeoRectBounds { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $bounds: Combine.Published.Publisher { + final public var $bounds: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($image) final public var image: UIKit.UIImage { + @Combine::Published @_projectedValueProperty($image) final public var image: UIKit::UIImage { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $image: Combine.Published.Publisher { + final public var $image: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($opacity) final public var opacity: Swift.Double { + @Combine::Published @_projectedValueProperty($opacity) final public var opacity: Swift::Double { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $opacity: Combine.Published.Publisher { + final public var $opacity: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($tileSize) final public var tileSize: Swift.Int { + @Combine::Published @_projectedValueProperty($tileSize) final public var tileSize: Swift::Int { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $tileSize: Combine.Published.Publisher { + final public var $tileSize: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($extra) final public var extra: Any? { + @Combine::Published @_projectedValueProperty($extra) final public var extra: Any? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $extra: Combine.Published.Publisher { + final public var $extra: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore.OnGroundImageEventHandler? { + @Combine::Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore::OnGroundImageEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onClick: Combine.Published.Publisher { + final public var $onClick: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - public init(bounds: MapConductorCore.GeoRectBounds, image: UIKit.UIImage, opacity: Swift.Double = 1.0, tileSize: Swift.Int = 512, id: Swift.String? = nil, extra: Any? = nil, onClick: MapConductorCore.OnGroundImageEventHandler? = nil) - final public func copy(bounds: MapConductorCore.GeoRectBounds? = nil, image: UIKit.UIImage? = nil, opacity: Swift.Double? = nil, tileSize: Swift.Int? = nil, id: Swift.String? = nil, extra: Any? = nil, onClick: MapConductorCore.OnGroundImageEventHandler? = nil) -> MapConductorCore.GroundImageState - final public func fingerPrint() -> MapConductorCore.GroundImageFingerPrint - final public func asFlow() -> Combine.AnyPublisher - public static func == (lhs: MapConductorCore.GroundImageState, rhs: MapConductorCore.GroundImageState) -> Swift.Bool - final public func hash(into hasher: inout Swift.Hasher) - final public func hashCode() -> Swift.Int - public typealias ID = Swift.String - public typealias ObjectWillChangePublisher = Combine.ObservableObjectPublisher + public init(bounds: MapConductorCore::GeoRectBounds, image: UIKit::UIImage, opacity: Swift::Double = 1.0, tileSize: Swift::Int = 512, id: Swift::String? = nil, extra: Any? = nil, onClick: MapConductorCore::OnGroundImageEventHandler? = nil) + final public func copy(bounds: MapConductorCore::GeoRectBounds? = nil, image: UIKit::UIImage? = nil, opacity: Swift::Double? = nil, tileSize: Swift::Int? = nil, id: Swift::String? = nil, extra: Any? = nil, onClick: MapConductorCore::OnGroundImageEventHandler? = nil) -> MapConductorCore::GroundImageState + final public func fingerPrint() -> MapConductorCore::GroundImageFingerPrint + final public func asFlow() -> Combine::AnyPublisher + public static func == (lhs: MapConductorCore::GroundImageState, rhs: MapConductorCore::GroundImageState) -> Swift::Bool + final public func hash(into hasher: inout Swift::Hasher) + final public func hashCode() -> Swift::Int + public typealias ID = Swift::String + public typealias ObjectWillChangePublisher = Combine::ObservableObjectPublisher @objc deinit - final public var hashValue: Swift.Int { + final public var hashValue: Swift::Int { get } } -final public class GroundImageTileProvider : MapConductorCore.TileProvider { - public static let defaultTileSize: Swift.Int - final public let tileSize: Swift.Int - public init(tileSize: Swift.Int = GroundImageTileProvider.defaultTileSize, cacheSizeKb: Swift.Int = 8 * 1024) - final public func update(state: MapConductorCore.GroundImageState, opacity: Swift.Double? = nil) - final public func renderTile(request: MapConductorCore.TileRequest) -> Foundation.Data? +final public class GroundImageTileProvider : MapConductorCore::TileProvider { + public static let defaultTileSize: Swift::Int + final public let tileSize: Swift::Int + public init(tileSize: Swift::Int = GroundImageTileProvider.defaultTileSize, cacheSizeKb: Swift::Int = 8 * 1024) + final public func update(state: MapConductorCore::GroundImageState, opacity: Swift::Double? = nil) + final public func renderTile(request: MapConductorCore::TileRequest) -> Foundation::Data? @objc deinit } -@_Concurrency.MainActor @preconcurrency public struct DefaultInfoBubbleView : SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public init(bubbleColor: SwiftUICore.Color = .white, borderColor: SwiftUICore.Color = .black, borderWidth: CoreFoundation.CGFloat = 2.0, contentPadding: CoreFoundation.CGFloat = 8.0, cornerRadius: CoreFoundation.CGFloat = 4.0, tailSize: CoreFoundation.CGFloat = 8.0, content: SwiftUICore.AnyView) - @_Concurrency.MainActor @preconcurrency public var body: some SwiftUICore.View { +@_Concurrency::MainActor @preconcurrency public struct DefaultInfoBubbleView : SwiftUICore::View { + @_Concurrency::MainActor @preconcurrency public init(bubbleColor: SwiftUICore::Color = .white, borderColor: SwiftUICore::Color = .black, borderWidth: CoreFoundation::CGFloat = 2.0, contentPadding: CoreFoundation::CGFloat = 8.0, cornerRadius: CoreFoundation::CGFloat = 4.0, tailSize: CoreFoundation::CGFloat = 8.0, content: SwiftUICore::AnyView) + @_Concurrency::MainActor @preconcurrency public var body: some SwiftUICore::View { get } public typealias Body = @_opaqueReturnTypeOf("$s16MapConductorCore21DefaultInfoBubbleViewV4bodyQrvp", 0) __ } public struct MarkerIconMetrics { - public let size: CoreFoundation.CGSize - public let anchor: CoreFoundation.CGPoint - public let infoAnchor: CoreFoundation.CGPoint - public init(size: CoreFoundation.CGSize, anchor: CoreFoundation.CGPoint, infoAnchor: CoreFoundation.CGPoint) -} -@_Concurrency.MainActor final public class InfoBubbleOverlayCoordinator { - public typealias Projection = (any MapConductorCore.GeoPointProtocol) -> CoreFoundation.CGPoint? - public typealias MarkerStateResolver = (_ markerId: Swift.String, _ bubbleMarker: MapConductorCore.MarkerState) -> MapConductorCore.MarkerState - public typealias IconMetricsProvider = (_ markerState: MapConductorCore.MarkerState) -> MapConductorCore.MarkerIconMetrics - @_Concurrency.MainActor public init(container: UIKit.UIView, project: @escaping MapConductorCore.InfoBubbleOverlayCoordinator.Projection, projectionGate: @escaping () -> Swift.Bool = { true }, resolveMarkerStateForIcon: @escaping MapConductorCore.InfoBubbleOverlayCoordinator.MarkerStateResolver = { _, bubbleMarker in bubbleMarker }, iconMetrics: @escaping MapConductorCore.InfoBubbleOverlayCoordinator.IconMetricsProvider) - @_Concurrency.MainActor final public func syncInfoBubbles(_ bubbles: [MapConductorCore.InfoBubble]) - @_Concurrency.MainActor final public func removeInfoBubbleView(for id: Swift.String) - @_Concurrency.MainActor final public func updateAllLayouts() - @_Concurrency.MainActor final public func updateInfoBubblePosition(for id: Swift.String) - @_Concurrency.MainActor final public func updateInfoBubblePosition(for id: Swift.String, screenPoint: CoreFoundation.CGPoint) - @_Concurrency.MainActor final public func unbind() - @objc deinit -} -@objc @_inheritsConvenienceInitializers @_Concurrency.MainActor @preconcurrency final public class PassthroughContainerView : UIKit.UIView { - @_Concurrency.MainActor @preconcurrency final public var onLayout: (() -> Swift.Void)? - @_Concurrency.MainActor @preconcurrency @objc override final public func layoutSubviews() - @_Concurrency.MainActor @preconcurrency @objc override final public func hitTest(_ point: CoreFoundation.CGPoint, with event: UIKit.UIEvent?) -> UIKit.UIView? - @_Concurrency.MainActor @preconcurrency @objc override dynamic public init(frame: CoreFoundation.CGRect) - @_Concurrency.MainActor @preconcurrency @objc required dynamic public init?(coder: Foundation.NSCoder) - @objc deinit -} -public struct AttributionRule : Swift.Hashable { - public let attribution: Swift.String - public let minZoom: Swift.Int? - public let maxZoom: Swift.Int? - public let bounds: MapConductorCore.GeoRectBounds? - public init(attribution: Swift.String, minZoom: Swift.Int? = nil, maxZoom: Swift.Int? = nil, bounds: MapConductorCore.GeoRectBounds? = nil) - public static func == (a: MapConductorCore.AttributionRule, b: MapConductorCore.AttributionRule) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -public func resolveAttributionRules(_ rules: [MapConductorCore.AttributionRule], camera: any MapConductorCore.MapCameraPositionProtocol) -> [Swift.String] -public func resolveMapAttributions(designRules: [MapConductorCore.AttributionRule], rasterLayers: [MapConductorCore.RasterLayer], camera: any MapConductorCore.MapCameraPositionProtocol) -> [Swift.String] -public struct CameraRestriction : Swift.Equatable { - public let bounds: MapConductorCore.GeoRectBounds? - public let minZoom: Swift.Double? - public let maxZoom: Swift.Double? - public init(bounds: MapConductorCore.GeoRectBounds? = nil, minZoom: Swift.Double? = nil, maxZoom: Swift.Double? = nil) - public var isEmpty: Swift.Bool { - get - } - public static let None: MapConductorCore.CameraRestriction - public static func == (lhs: MapConductorCore.CameraRestriction, rhs: MapConductorCore.CameraRestriction) -> Swift.Bool + public let size: CoreFoundation::CGSize + public let anchor: CoreFoundation::CGPoint + public let infoAnchor: CoreFoundation::CGPoint + public init(size: CoreFoundation::CGSize, anchor: CoreFoundation::CGPoint, infoAnchor: CoreFoundation::CGPoint) +} +@_Concurrency::MainActor final public class InfoBubbleOverlayCoordinator { + public typealias Projection = (any MapConductorCore::GeoPointProtocol) -> CoreFoundation::CGPoint? + public typealias MarkerStateResolver = (_ markerId: Swift::String, _ bubbleMarker: MapConductorCore::MarkerState) -> MapConductorCore::MarkerState + public typealias IconMetricsProvider = (_ markerState: MapConductorCore::MarkerState) -> MapConductorCore::MarkerIconMetrics + @_Concurrency::MainActor public init(container: UIKit::UIView, project: @escaping MapConductorCore::InfoBubbleOverlayCoordinator.MapConductorCore::Projection, projectionGate: @escaping () -> Swift::Bool = { true }, resolveMarkerStateForIcon: @escaping MapConductorCore::InfoBubbleOverlayCoordinator.MapConductorCore::MarkerStateResolver = { _, bubbleMarker in bubbleMarker }, iconMetrics: @escaping MapConductorCore::InfoBubbleOverlayCoordinator.MapConductorCore::IconMetricsProvider) + @_Concurrency::MainActor final public func syncInfoBubbles(_ bubbles: [MapConductorCore::InfoBubble]) + @_Concurrency::MainActor final public func removeInfoBubbleView(for id: Swift::String) + @_Concurrency::MainActor final public func updateAllLayouts() + @_Concurrency::MainActor final public func updateInfoBubblePosition(for id: Swift::String) + @_Concurrency::MainActor final public func updateInfoBubblePosition(for id: Swift::String, screenPoint: CoreFoundation::CGPoint) + @_Concurrency::MainActor final public func unbind() + @objc deinit +} +@objc @_inheritsConvenienceInitializers @_Concurrency::MainActor @preconcurrency final public class PassthroughContainerView : UIKit::UIView { + @_Concurrency::MainActor @preconcurrency final public var onLayout: (() -> Swift::Void)? + @_Concurrency::MainActor @preconcurrency @objc override final public func layoutSubviews() + @_Concurrency::MainActor @preconcurrency @objc override final public func hitTest(_ point: CoreFoundation::CGPoint, with event: UIKit::UIEvent?) -> UIKit::UIView? + @_Concurrency::MainActor @preconcurrency @objc override dynamic public init(frame: CoreFoundation::CGRect) + @_Concurrency::MainActor @preconcurrency @objc required dynamic public init?(coder: Foundation::NSCoder) + @objc deinit +} +public protocol StyleMutationTarget { + var isReady: Swift::Bool { get } + func hasLayer(_ layerId: Swift::String) -> Swift::Bool + func setPaint(_ layerId: Swift::String, _ key: Swift::String, _ valueJson: Swift::String) throws + func setLayout(_ layerId: Swift::String, _ key: Swift::String, _ valueJson: Swift::String) throws + func setFilter(_ layerId: Swift::String, _ filterJson: Swift::String) throws + func setZoomRange(_ layerId: Swift::String, _ minZoom: Swift::Double, _ maxZoom: Swift::Double) throws + func addLayer(_ layerId: Swift::String, _ layerJson: Swift::String, _ beforeId: Swift::String?) throws + func removeLayer(_ layerId: Swift::String) throws +} +extension MapConductorCore::StyleMutationTarget { + public var isReady: Swift::Bool { + get + } +} +public func applyStyleMutations(_ target: any MapConductorCore::StyleMutationTarget, _ mutations: [MapConductorCore::StyleMutation]) -> [MapConductorCore::StyleMutation] +public func describeStyleMutations(_ mutations: [MapConductorCore::StyleMutation]) -> Swift::String +public struct AttributionRule : Swift::Hashable { + public let attribution: Swift::String + public let minZoom: Swift::Int? + public let maxZoom: Swift::Int? + public let bounds: MapConductorCore::GeoRectBounds? + public init(attribution: Swift::String, minZoom: Swift::Int? = nil, maxZoom: Swift::Int? = nil, bounds: MapConductorCore::GeoRectBounds? = nil) + public static func == (a: MapConductorCore::AttributionRule, b: MapConductorCore::AttributionRule) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { + get + } +} +public func resolveAttributionRules(_ rules: [MapConductorCore::AttributionRule], camera: any MapConductorCore::MapCameraPositionProtocol) -> [Swift::String] +public func resolveMapAttributions(designRules: [MapConductorCore::AttributionRule], rasterLayers: [MapConductorCore::RasterLayer], camera: any MapConductorCore::MapCameraPositionProtocol) -> [Swift::String] +public enum BlankMapStyle { + public static let backgroundColor: Swift::String + public static let json: Swift::String + public static let fileURL: Foundation::URL +} +public struct CameraRestriction : Swift::Equatable { + public let bounds: MapConductorCore::GeoRectBounds? + public let minZoom: Swift::Double? + public let maxZoom: Swift::Double? + public init(bounds: MapConductorCore::GeoRectBounds? = nil, minZoom: Swift::Double? = nil, maxZoom: Swift::Double? = nil) + public var isEmpty: Swift::Bool { + get + } + public static let None: MapConductorCore::CameraRestriction + public static func == (lhs: MapConductorCore::CameraRestriction, rhs: MapConductorCore::CameraRestriction) -> Swift::Bool } final public class CameraRestrictionClamp { - public static let zoomEps: Swift.Double - public static let coordEps: Swift.Double + public static let zoomEps: Swift::Double + public static let coordEps: Swift::Double public init() - final public func set(_ restriction: MapConductorCore.CameraRestriction?) - final public var current: MapConductorCore.CameraRestriction? { + final public func set(_ restriction: MapConductorCore::CameraRestriction?) + final public var current: MapConductorCore::CameraRestriction? { get } - final public var hasRestriction: Swift.Bool { + final public var hasRestriction: Swift::Bool { get } - final public func correction(for current: MapConductorCore.MapCameraPosition) -> MapConductorCore.MapCameraPosition? + final public func correction(for current: MapConductorCore::MapCameraPosition) -> MapConductorCore::MapCameraPosition? @objc deinit } -@_Concurrency.MainActor @preconcurrency public struct MapAttributionOverlay : SwiftUICore.View { - @_Concurrency.MainActor @preconcurrency public init(designRules: [MapConductorCore.AttributionRule], rasterLayers: [MapConductorCore.RasterLayer], camera: any MapConductorCore.MapCameraPositionProtocol) - @_Concurrency.MainActor @preconcurrency public var body: some SwiftUICore.View { +@_Concurrency::MainActor @preconcurrency public struct MapAttributionOverlay : SwiftUICore::View { + @_Concurrency::MainActor @preconcurrency public init(designRules: [MapConductorCore::AttributionRule], rasterLayers: [MapConductorCore::RasterLayer], camera: any MapConductorCore::MapCameraPositionProtocol) + @_Concurrency::MainActor @preconcurrency public var body: some SwiftUICore::View { get } public typealias Body = @_opaqueReturnTypeOf("$s16MapConductorCore0A18AttributionOverlayV4bodyQrvp", 0) __ } -public struct VisibleRegion : Swift.Equatable, Swift.Hashable { - public let bounds: MapConductorCore.GeoRectBounds - public let nearLeft: MapConductorCore.GeoPoint? - public let nearRight: MapConductorCore.GeoPoint? - public let farLeft: MapConductorCore.GeoPoint? - public let farRight: MapConductorCore.GeoPoint? - public init(bounds: MapConductorCore.GeoRectBounds, nearLeft: MapConductorCore.GeoPoint? = nil, nearRight: MapConductorCore.GeoPoint? = nil, farLeft: MapConductorCore.GeoPoint? = nil, farRight: MapConductorCore.GeoPoint? = nil) - public static func == (lhs: MapConductorCore.VisibleRegion, rhs: MapConductorCore.VisibleRegion) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public struct VisibleRegion : Swift::Equatable, Swift::Hashable { + public let bounds: MapConductorCore::GeoRectBounds + public let nearLeft: MapConductorCore::GeoPoint? + public let nearRight: MapConductorCore::GeoPoint? + public let farLeft: MapConductorCore::GeoPoint? + public let farRight: MapConductorCore::GeoPoint? + public init(bounds: MapConductorCore::GeoRectBounds, nearLeft: MapConductorCore::GeoPoint? = nil, nearRight: MapConductorCore::GeoPoint? = nil, farLeft: MapConductorCore::GeoPoint? = nil, farRight: MapConductorCore::GeoPoint? = nil) + public static func == (lhs: MapConductorCore::VisibleRegion, rhs: MapConductorCore::VisibleRegion) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public protocol MapCameraPositionProtocol { - var position: any MapConductorCore.GeoPointProtocol { get } - var zoom: Swift.Double { get } - var bearing: Swift.Double { get } - var tilt: Swift.Double { get } - var paddings: (any MapConductorCore.MapPaddingsProtocol)? { get } - var visibleRegion: MapConductorCore.VisibleRegion? { get } -} -final public class MapCameraPosition : MapConductorCore.MapCameraPositionProtocol { - final public var position: any MapConductorCore.GeoPointProtocol { - get - } - final public let zoom: Swift.Double - final public let bearing: Swift.Double - final public let tilt: Swift.Double - final public let paddings: (any MapConductorCore.MapPaddingsProtocol)? - final public let visibleRegion: MapConductorCore.VisibleRegion? - public init(position: any MapConductorCore.GeoPointProtocol, zoom: Swift.Double = 0.0, bearing: Swift.Double = 0.0, tilt: Swift.Double = 0.0, paddings: (any MapConductorCore.MapPaddingsProtocol)? = MapPaddings.Zeros, visibleRegion: MapConductorCore.VisibleRegion? = nil) - final public func equals(other: any MapConductorCore.MapCameraPositionProtocol) -> Swift.Bool - final public func copy(position: (any MapConductorCore.GeoPointProtocol)? = nil, zoom: Swift.Double? = nil, bearing: Swift.Double? = nil, tilt: Swift.Double? = nil, paddings: (any MapConductorCore.MapPaddingsProtocol)? = nil, visibleRegion: MapConductorCore.VisibleRegion? = nil) -> MapConductorCore.MapCameraPosition - final public func hashCode() -> Swift.Int - public static let Default: MapConductorCore.MapCameraPosition - @objc deinit -} -public enum MapCapability : Swift.String, Swift.CaseIterable, Swift.Sendable { + var position: any MapConductorCore::GeoPointProtocol { get } + var zoom: Swift::Double { get } + var bearing: Swift::Double { get } + var tilt: Swift::Double { get } + var paddings: (any MapConductorCore::MapPaddingsProtocol)? { get } + var visibleRegion: MapConductorCore::VisibleRegion? { get } +} +final public class MapCameraPosition : MapConductorCore::MapCameraPositionProtocol { + final public var position: any MapConductorCore::GeoPointProtocol { + get + } + final public let zoom: Swift::Double + final public let bearing: Swift::Double + final public let tilt: Swift::Double + final public let paddings: (any MapConductorCore::MapPaddingsProtocol)? + final public let visibleRegion: MapConductorCore::VisibleRegion? + public init(position: any MapConductorCore::GeoPointProtocol, zoom: Swift::Double = 0.0, bearing: Swift::Double = 0.0, tilt: Swift::Double = 0.0, paddings: (any MapConductorCore::MapPaddingsProtocol)? = MapPaddings.Zeros, visibleRegion: MapConductorCore::VisibleRegion? = nil) + final public func equals(other: any MapConductorCore::MapCameraPositionProtocol) -> Swift::Bool + final public func copy(position: (any MapConductorCore::GeoPointProtocol)? = nil, zoom: Swift::Double? = nil, bearing: Swift::Double? = nil, tilt: Swift::Double? = nil, paddings: (any MapConductorCore::MapPaddingsProtocol)? = nil, visibleRegion: MapConductorCore::VisibleRegion? = nil) -> MapConductorCore::MapCameraPosition + final public func hashCode() -> Swift::Int + public static let Default: MapConductorCore::MapCameraPosition + @objc deinit +} +public enum MapCapability : Swift::String, Swift::CaseIterable, Swift::Sendable { case marker case polyline case polygon @@ -1212,122 +1229,122 @@ public enum MapCapability : Swift.String, Swift.CaseIterable, Swift.Sendable { case gestureZoom case gestureRotate case gestureTilt - public var id: Swift.String { + public var id: Swift::String { get } - public static func fromId(_ id: Swift.String) -> MapConductorCore.MapCapability? - public init?(rawValue: Swift.String) - public typealias AllCases = [MapConductorCore.MapCapability] - public typealias RawValue = Swift.String - nonisolated public static var allCases: [MapConductorCore.MapCapability] { + public static func fromId(_ id: Swift::String) -> MapConductorCore::MapCapability? + public init?(rawValue: Swift::String) + public typealias AllCases = [MapConductorCore::MapCapability] + public typealias RawValue = Swift::String + nonisolated public static var allCases: [MapConductorCore::MapCapability] { get } - public var rawValue: Swift.String { + public var rawValue: Swift::String { get } } -extension MapConductorCore.MapGesture { - public var capability: MapConductorCore.MapCapability { +extension MapConductorCore::MapGesture { + public var capability: MapConductorCore::MapCapability { get } } -public enum MapCapabilityStatus : Swift.Equatable, Swift.Sendable { +public enum MapCapabilityStatus : Swift::Equatable, Swift::Sendable { case supported - case degraded(Swift.String) - case approximated(Swift.String) - case unsupported(Swift.String) + case degraded(Swift::String) + case approximated(Swift::String) + case unsupported(Swift::String) case unknown - public var reason: Swift.String? { + public var reason: Swift::String? { get } - public var isFullySupported: Swift.Bool { + public var isFullySupported: Swift::Bool { get } - public var isUsable: Swift.Bool { + public var isUsable: Swift::Bool { get } - public var isKnownUnsupported: Swift.Bool { + public var isKnownUnsupported: Swift::Bool { get } - public static func == (a: MapConductorCore.MapCapabilityStatus, b: MapConductorCore.MapCapabilityStatus) -> Swift.Bool + public static func == (a: MapConductorCore::MapCapabilityStatus, b: MapConductorCore::MapCapabilityStatus) -> Swift::Bool } public protocol MapDesignTypeProtocol { associatedtype Identifier var id: Self.Identifier { get } - var attributionRules: [MapConductorCore.AttributionRule] { get } + var attributionRules: [MapConductorCore::AttributionRule] { get } func getValue() -> Self.Identifier } -extension MapConductorCore.MapDesignTypeProtocol { - public var attributionRules: [MapConductorCore.AttributionRule] { +extension MapConductorCore::MapDesignTypeProtocol { + public var attributionRules: [MapConductorCore::AttributionRule] { get } } public enum MapDiagnostics { - public typealias Sink = @Sendable (Swift.String) -> Swift.Void - public static var sink: MapConductorCore.MapDiagnostics.Sink { + public typealias Sink = @Sendable (Swift::String) -> Swift::Void + public static var sink: MapConductorCore::MapDiagnostics.MapConductorCore::Sink { get set } @discardableResult - public static func report(capability: MapConductorCore.MapCapability, level: MapConductorCore.MapDiagnosticLevel, provider: Swift.String, reason: Swift.String, subject: Swift.String? = nil) -> Swift.Bool + public static func report(capability: MapConductorCore::MapCapability, level: MapConductorCore::MapDiagnosticLevel, provider: Swift::String, reason: Swift::String, subject: Swift::String? = nil) -> Swift::Bool @discardableResult - public static func reportIfRequested(_ requested: Swift.Bool, capability: MapConductorCore.MapCapability, level: MapConductorCore.MapDiagnosticLevel, provider: Swift.String, reason: Swift.String, subject: Swift.String? = nil) -> Swift.Bool + public static func reportIfRequested(_ requested: Swift::Bool, capability: MapConductorCore::MapCapability, level: MapConductorCore::MapDiagnosticLevel, provider: Swift::String, reason: Swift::String, subject: Swift::String? = nil) -> Swift::Bool public static func resetWarnings() } -public enum MapDiagnosticLevel : Swift.String, Swift.Sendable { +public enum MapDiagnosticLevel : Swift::String, Swift::Sendable { case unsupported case degraded case approximated case ignored - public init?(rawValue: Swift.String) - public typealias RawValue = Swift.String - public var rawValue: Swift.String { + public init?(rawValue: Swift::String) + public typealias RawValue = Swift::String + public var rawValue: Swift::String { get } } public protocol MapPaddingsProtocol { - var top: Swift.Double { get } - var left: Swift.Double { get } - var bottom: Swift.Double { get } - var right: Swift.Double { get } -} -open class MapPaddings : MapConductorCore.MapPaddingsProtocol { - final public let top: Swift.Double - final public let left: Swift.Double - final public let bottom: Swift.Double - final public let right: Swift.Double - public init(top: Swift.Double = 0.0, left: Swift.Double = 0.0, bottom: Swift.Double = 0.0, right: Swift.Double = 0.0) - public static let Zeros: MapConductorCore.MapPaddings - public static func from(paddings: any MapConductorCore.MapPaddingsProtocol) -> MapConductorCore.MapPaddings - public func hashCode() -> Swift.Int - @objc deinit -} -public enum MapProjection : Swift.Hashable, Swift.Sendable { + var top: Swift::Double { get } + var left: Swift::Double { get } + var bottom: Swift::Double { get } + var right: Swift::Double { get } +} +open class MapPaddings : MapConductorCore::MapPaddingsProtocol { + final public let top: Swift::Double + final public let left: Swift::Double + final public let bottom: Swift::Double + final public let right: Swift::Double + public init(top: Swift::Double = 0.0, left: Swift::Double = 0.0, bottom: Swift::Double = 0.0, right: Swift::Double = 0.0) + public static let Zeros: MapConductorCore::MapPaddings + public static func from(paddings: any MapConductorCore::MapPaddingsProtocol) -> MapConductorCore::MapPaddings + public func hashCode() -> Swift::Int + @objc deinit +} +public enum MapProjection : Swift::Hashable, Swift::Sendable { case mercator case globe - public static func == (a: MapConductorCore.MapProjection, b: MapConductorCore.MapProjection) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { + public static func == (a: MapConductorCore::MapProjection, b: MapConductorCore::MapProjection) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public protocol MapServiceKey { associatedtype Value - static var capability: MapConductorCore.MapCapability? { get } + static var capability: MapConductorCore::MapCapability? { get } } -extension MapConductorCore.MapServiceKey { - public static var capability: MapConductorCore.MapCapability? { +extension MapConductorCore::MapServiceKey { + public static var capability: MapConductorCore::MapCapability? { get } } public protocol MapServiceRegistry : AnyObject { - func get(_ key: Key.Type) -> Key.Value? where Key : MapConductorCore.MapServiceKey - func has(_ key: Key.Type) -> Swift.Bool where Key : MapConductorCore.MapServiceKey - func capabilityStatus(_ capability: MapConductorCore.MapCapability) -> MapConductorCore.MapCapabilityStatus + func get(_ key: Key.Type) -> Key.Value? where Key : MapConductorCore::MapServiceKey + func has(_ key: Key.Type) -> Swift::Bool where Key : MapConductorCore::MapServiceKey + func capabilityStatus(_ capability: MapConductorCore::MapCapability) -> MapConductorCore::MapCapabilityStatus } -extension MapConductorCore.MapServiceRegistry { - public func has(_ key: Key.Type) -> Swift.Bool where Key : MapConductorCore.MapServiceKey - public func capabilityStatus(_: MapConductorCore.MapCapability) -> MapConductorCore.MapCapabilityStatus +extension MapConductorCore::MapServiceRegistry { + public func has(_ key: Key.Type) -> Swift::Bool where Key : MapConductorCore::MapServiceKey + public func capabilityStatus(_: MapConductorCore::MapCapability) -> MapConductorCore::MapCapabilityStatus } @_hasMissingDesignatedInitializers final public class MapServiceRegistration { final public func dispose() @@ -1336,64 +1353,64 @@ extension MapConductorCore.MapServiceRegistry { final public class MapServiceRegistrations { public init() @discardableResult - final public func add(_ registration: MapConductorCore.MapServiceRegistration) -> MapConductorCore.MapServiceRegistration + final public func add(_ registration: MapConductorCore::MapServiceRegistration) -> MapConductorCore::MapServiceRegistration final public func disposeAll() @objc deinit } -final public class MutableMapServiceRegistry : MapConductorCore.MapServiceRegistry { +final public class MutableMapServiceRegistry : MapConductorCore::MapServiceRegistry { public init() - final public func put(_ key: Key.Type, _ value: Key.Value) where Key : MapConductorCore.MapServiceKey + final public func put(_ key: Key.Type, _ value: Key.Value) where Key : MapConductorCore::MapServiceKey @discardableResult - final public func register(_ key: Key.Type, _ value: Key.Value) -> MapConductorCore.MapServiceRegistration where Key : MapConductorCore.MapServiceKey - final public func remove(_ key: Key.Type) where Key : MapConductorCore.MapServiceKey + final public func register(_ key: Key.Type, _ value: Key.Value) -> MapConductorCore::MapServiceRegistration where Key : MapConductorCore::MapServiceKey + final public func remove(_ key: Key.Type) where Key : MapConductorCore::MapServiceKey final public func clear() @discardableResult - final public func declare(_ capability: MapConductorCore.MapCapability, _ status: MapConductorCore.MapCapabilityStatus) -> MapConductorCore.MapServiceRegistration + final public func declare(_ capability: MapConductorCore::MapCapability, _ status: MapConductorCore::MapCapabilityStatus) -> MapConductorCore::MapServiceRegistration @discardableResult - final public func declareUnsupported(_ capability: MapConductorCore.MapCapability, _ reason: Swift.String) -> MapConductorCore.MapServiceRegistration - final public func declaredCapabilities() -> [MapConductorCore.MapCapability : MapConductorCore.MapCapabilityStatus] - final public func capabilityStatus(_ capability: MapConductorCore.MapCapability) -> MapConductorCore.MapCapabilityStatus - final public func get(_ key: Key.Type) -> Key.Value? where Key : MapConductorCore.MapServiceKey + final public func declareUnsupported(_ capability: MapConductorCore::MapCapability, _ reason: Swift::String) -> MapConductorCore::MapServiceRegistration + final public func declaredCapabilities() -> [MapConductorCore::MapCapability : MapConductorCore::MapCapabilityStatus] + final public func capabilityStatus(_ capability: MapConductorCore::MapCapability) -> MapConductorCore::MapCapabilityStatus + final public func get(_ key: Key.Type) -> Key.Value? where Key : MapConductorCore::MapServiceKey @objc deinit } -extension MapConductorCore.MutableMapServiceRegistry { +extension MapConductorCore::MutableMapServiceRegistry { final public func removeProviderRegistrations() } -@_hasMissingDesignatedInitializers final public class EmptyMapServiceRegistry : MapConductorCore.MapServiceRegistry { - public static let shared: MapConductorCore.EmptyMapServiceRegistry - final public func get(_: Key.Type) -> Key.Value? where Key : MapConductorCore.MapServiceKey +@_hasMissingDesignatedInitializers final public class EmptyMapServiceRegistry : MapConductorCore::MapServiceRegistry { + public static let shared: MapConductorCore::EmptyMapServiceRegistry + final public func get(_: Key.Type) -> Key.Value? where Key : MapConductorCore::MapServiceKey @objc deinit } -@_Concurrency.MainActor public enum MapServiceRegistryScope { - @_Concurrency.MainActor public static var current: any MapConductorCore.MapServiceRegistry { +@_Concurrency::MainActor public enum MapServiceRegistryScope { + @_Concurrency::MainActor public static var current: any MapConductorCore::MapServiceRegistry { get } - @_Concurrency.MainActor public static func with(_ registry: any MapConductorCore.MapServiceRegistry, _ body: () -> Result) -> Result + @_Concurrency::MainActor public static func with(_ registry: any MapConductorCore::MapServiceRegistry, _ body: () -> Result) -> Result } public protocol MapViewHolderProtocol { associatedtype ActualMapView associatedtype ActualMap var mapView: Self.ActualMapView { get } var map: Self.ActualMap { get } - func toScreenOffset(position: any MapConductorCore.GeoPointProtocol) -> CoreFoundation.CGPoint? - func fromScreenOffset(offset: CoreFoundation.CGPoint) async -> MapConductorCore.GeoPoint? - func fromScreenOffsetSync(offset: CoreFoundation.CGPoint) -> MapConductorCore.GeoPoint? - func viewportSizePx() -> CoreFoundation.CGSize? + func toScreenOffset(position: any MapConductorCore::GeoPointProtocol) -> CoreFoundation::CGPoint? + func fromScreenOffset(offset: CoreFoundation::CGPoint) async -> MapConductorCore::GeoPoint? + func fromScreenOffsetSync(offset: CoreFoundation::CGPoint) -> MapConductorCore::GeoPoint? + func viewportSizePx() -> CoreFoundation::CGSize? } -extension MapConductorCore.MapViewHolderProtocol { - public func fromScreenOffset(offset: CoreFoundation.CGPoint) async -> MapConductorCore.GeoPoint? - public func fromScreenOffsetSync(offset _: CoreFoundation.CGPoint) -> MapConductorCore.GeoPoint? +extension MapConductorCore::MapViewHolderProtocol { + public func fromScreenOffset(offset: CoreFoundation::CGPoint) async -> MapConductorCore::GeoPoint? + public func fromScreenOffsetSync(offset _: CoreFoundation::CGPoint) -> MapConductorCore::GeoPoint? } -public struct AnyMapViewHolder : MapConductorCore.MapViewHolderProtocol { +public struct AnyMapViewHolder : MapConductorCore::MapViewHolderProtocol { public typealias ActualMapView = Any public typealias ActualMap = Any public let mapView: Any public let map: Any - public init(_ holder: H) where H : MapConductorCore.MapViewHolderProtocol - public func toScreenOffset(position: any MapConductorCore.GeoPointProtocol) -> CoreFoundation.CGPoint? - public func fromScreenOffset(offset: CoreFoundation.CGPoint) async -> MapConductorCore.GeoPoint? - public func fromScreenOffsetSync(offset: CoreFoundation.CGPoint) -> MapConductorCore.GeoPoint? - public func viewportSizePx() -> CoreFoundation.CGSize? + public init(_ holder: H) where H : MapConductorCore::MapViewHolderProtocol + public func toScreenOffset(position: any MapConductorCore::GeoPointProtocol) -> CoreFoundation::CGPoint? + public func fromScreenOffset(offset: CoreFoundation::CGPoint) async -> MapConductorCore::GeoPoint? + public func fromScreenOffsetSync(offset: CoreFoundation::CGPoint) -> MapConductorCore::GeoPoint? + public func viewportSizePx() -> CoreFoundation::CGSize? } public enum InitState { case NotStarted @@ -1404,168 +1421,287 @@ public enum InitState { case MapCreated case MapLoaded case Failed - public static func == (a: MapConductorCore.InitState, b: MapConductorCore.InitState) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { + public static func == (a: MapConductorCore::InitState, b: MapConductorCore::InitState) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } -public protocol MapViewStateProtocol : Combine.ObservableObject { +public protocol MapViewStateProtocol : Combine::ObservableObject { associatedtype ActualMapDesignType - var id: Swift.String { get } - var cameraPosition: MapConductorCore.MapCameraPosition { get } + var id: Swift::String { get } + var cameraPosition: MapConductorCore::MapCameraPosition { get } var mapDesignType: Self.ActualMapDesignType { get set } - var uiSettings: MapConductorCore.MapUISettings { get set } - var serviceRegistry: MapConductorCore.MutableMapServiceRegistry { get } - func moveCameraTo(cameraPosition: MapConductorCore.MapCameraPosition, durationMillis: MapConductorCore.Long?) - func moveCameraTo(position: MapConductorCore.GeoPoint, durationMillis: MapConductorCore.Long?) - func fitBounds(bounds: MapConductorCore.GeoRectBounds, padding: Swift.Int) - func getMapViewHolder() -> MapConductorCore.AnyMapViewHolder? + var uiSettings: MapConductorCore::MapUISettings { get set } + var serviceRegistry: MapConductorCore::MutableMapServiceRegistry { get } + func moveCameraTo(cameraPosition: MapConductorCore::MapCameraPosition, durationMillis: MapConductorCore::Long?) + func moveCameraTo(position: MapConductorCore::GeoPoint, durationMillis: MapConductorCore::Long?) + func fitBounds(bounds: MapConductorCore::GeoRectBounds, padding: Swift::Int) + func getMapViewHolder() -> MapConductorCore::AnyMapViewHolder? } -extension MapConductorCore.MapViewStateProtocol { - public func moveCameraTo(cameraPosition: MapConductorCore.MapCameraPosition) - public func moveCameraTo(position: MapConductorCore.GeoPoint) +extension MapConductorCore::MapViewStateProtocol { + public func moveCameraTo(cameraPosition: MapConductorCore::MapCameraPosition) + public func moveCameraTo(position: MapConductorCore::GeoPoint) } -open class MapViewState : Combine.ObservableObject, MapConductorCore.MapViewStateProtocol { - final public let serviceRegistry: MapConductorCore.MutableMapServiceRegistry - public var attachedMapController: (any MapConductorCore.MapViewControllerProtocol)? { +open class MapViewState : Combine::ObservableObject, MapConductorCore::MapViewStateProtocol { + final public let serviceRegistry: MapConductorCore::MutableMapServiceRegistry + public var attachedMapController: (any MapConductorCore::MapViewControllerProtocol)? { get } - public init(id: Swift.String = UUID().uuidString, initialCameraPosition: MapConductorCore.MapCameraPosition = .Default, uiSettings: MapConductorCore.MapUISettings = MapUISettings(), optimisticCameraUpdate: Swift.Bool = false) - open var id: Swift.String { + public init(id: Swift::String = UUID().uuidString, initialCameraPosition: MapConductorCore::MapCameraPosition = .Default, uiSettings: MapConductorCore::MapUISettings = MapUISettings(), optimisticCameraUpdate: Swift::Bool = false) + open var id: Swift::String { get } - open var cameraPosition: MapConductorCore.MapCameraPosition { + open var cameraPosition: MapConductorCore::MapCameraPosition { get } open var mapDesignType: ActualMapDesignType { get set } - open var uiSettings: MapConductorCore.MapUISettings { + open var uiSettings: MapConductorCore::MapUISettings { get set } - public func attachController(_ controller: (any MapConductorCore.MapViewControllerProtocol)?, moveToInitialCamera: Swift.Bool = true) + public func attachController(_ controller: (any MapConductorCore::MapViewControllerProtocol)?, moveToInitialCamera: Swift::Bool = true) public func detachController() - public func setCameraPositionInternal(_ cameraPosition: MapConductorCore.MapCameraPosition) - open func moveCameraTo(cameraPosition: MapConductorCore.MapCameraPosition, durationMillis: MapConductorCore.Long? = 0) - open func moveCameraTo(position: MapConductorCore.GeoPoint, durationMillis: MapConductorCore.Long? = 0) - open func fitBounds(bounds: MapConductorCore.GeoRectBounds, padding: Swift.Int) - open func getMapViewHolder() -> MapConductorCore.AnyMapViewHolder? - public typealias ObjectWillChangePublisher = Combine.ObservableObjectPublisher + public func setCameraPositionInternal(_ cameraPosition: MapConductorCore::MapCameraPosition) + open func moveCameraTo(cameraPosition: MapConductorCore::MapCameraPosition, durationMillis: MapConductorCore::Long? = 0) + open func moveCameraTo(position: MapConductorCore::GeoPoint, durationMillis: MapConductorCore::Long? = 0) + open func fitBounds(bounds: MapConductorCore::GeoRectBounds, padding: Swift::Int) + open func getMapViewHolder() -> MapConductorCore::AnyMapViewHolder? + public typealias ObjectWillChangePublisher = Combine::ObservableObjectPublisher @objc deinit } public protocol MapOverlayProtocol : AnyObject { associatedtype DataType - var flow: Combine.CurrentValueSubject<[Swift.String : Self.DataType], Swift.Never> { get } - func render(data: [Swift.String : Self.DataType], controller: any MapConductorCore.MapViewControllerProtocol) async + var flow: Combine::CurrentValueSubject<[Swift::String : Self.DataType], Swift::Never> { get } + func render(data: [Swift::String : Self.DataType], controller: any MapConductorCore::MapViewControllerProtocol) async } final public class MapOverlayRegistry { public init() - final public func register(overlay: any MapConductorCore.MapOverlayProtocol) - final public func getAll() -> [any MapConductorCore.MapOverlayProtocol] + final public func register(overlay: any MapConductorCore::MapOverlayProtocol) + final public func getAll() -> [any MapConductorCore::MapOverlayProtocol] @objc deinit } +public protocol MapViewStyle { + var key: Swift::String { get } + func install(host: any MapConductorCore::MapStyleHost) -> MapConductorCore::MapStyleInstallation +} +public struct MapStyleInstallation { + public init(dispose: @escaping () -> Swift::Void, update: @escaping (any MapConductorCore::MapViewStyle) -> Swift::Bool = { _ in false }) + public func dispose() + public func update(_ next: any MapConductorCore::MapViewStyle) -> Swift::Bool + public static let none: MapConductorCore::MapStyleInstallation +} +public protocol MapStyleHost : AnyObject { + var serviceRegistry: any MapConductorCore::MapServiceRegistry { get } + var tileServer: MapConductorCore::LocalTileServer { get } + var vectorStyleUrl: Swift::String? { get } + func onStyleLoaded(_ block: @escaping () -> Swift::Void) -> MapConductorCore::MapStyleInstallation + func upsertRaster(_ state: MapConductorCore::RasterLayerState) + func removeRaster(id: Swift::String) + func report(_ diagnostics: [Swift::String]) +} +extension MapConductorCore::MapStyleHost { + public var vectorStyleUrl: Swift::String? { + get + } +} +public protocol RasterLayerMounting : AnyObject { + func upsert(state: MapConductorCore::RasterLayerState) async + func removeById(_ id: Swift::String) async +} +final public class MapViewStyleHost : MapConductorCore::MapStyleHost { + final public let serviceRegistry: any MapConductorCore::MapServiceRegistry + public init(serviceRegistry: any MapConductorCore::MapServiceRegistry) + @objc deinit + final public func apply(_ style: (any MapConductorCore::MapViewStyle)?, onDiagnostics: (([Swift::String]) -> Swift::Void)? = nil) + final public func dispose() + final public func notifyStyleLoaded() + final public var tileServer: MapConductorCore::LocalTileServer { + get + } + final public var vectorStyleUrl: Swift::String? { + get + } + final public func onStyleLoaded(_ block: @escaping () -> Swift::Void) -> MapConductorCore::MapStyleInstallation + final public func upsertRaster(_ state: MapConductorCore::RasterLayerState) + final public func removeRaster(id: Swift::String) + final public func report(_ diagnostics: [Swift::String]) +} +extension MapConductorCore::MapViewCoordinatorBase { + @_Concurrency::MainActor public func applyStyle(_ style: (any MapConductorCore::MapViewStyle)?, onDiagnostics: (([Swift::String]) -> Swift::Void)? = nil) + @_Concurrency::MainActor public func disposeStyle() + @_Concurrency::MainActor public func notifyStyleLoaded() +} +@_Concurrency::MainActor @preconcurrency public struct MapViewStyleApplier : SwiftUI::UIViewRepresentable { + @_Concurrency::MainActor @preconcurrency public init(host: MapConductorCore::MapViewStyleHost, style: (any MapConductorCore::MapViewStyle)?, onDiagnostics: (([Swift::String]) -> Swift::Void)? = nil) + @_Concurrency::MainActor @preconcurrency public func makeUIView(context _: MapConductorCore::MapViewStyleApplier.SwiftUI::Context) -> UIKit::UIView + @_Concurrency::MainActor @preconcurrency public func updateUIView(_: UIKit::UIView, context _: MapConductorCore::MapViewStyleApplier.SwiftUI::Context) + public typealias Body = Swift::Never + public typealias Coordinator = Swift::Void + public typealias UIViewType = UIKit::UIView +} public enum ScreenProjectionRequirement { @discardableResult - public static func check(registry: any MapConductorCore.MapServiceRegistry, provider: Swift.String, feature: Swift.String) -> Swift.Bool -} -extension MapConductorCore.MapViewHolderProtocol { - public func buildVisibleRegion(inset: CoreFoundation.CGFloat = 0, requireAllCorners: Swift.Bool = true) -> MapConductorCore.VisibleRegion? - public func buildVisibleRegion(size: CoreFoundation.CGSize, inset: CoreFoundation.CGFloat = 0, requireAllCorners: Swift.Bool = true) -> MapConductorCore.VisibleRegion? - public func viewportSizePx() -> CoreFoundation.CGSize? -} -open class AbstractMarkerController : MapConductorCore.OverlayControllerProtocol, MapConductorCore.SlottedOverlayController where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol { - public typealias StateType = MapConductorCore.MarkerState - public typealias EntityType = MapConductorCore.MarkerEntity - public typealias EventType = MapConductorCore.MarkerState - final public let markerManager: MapConductorCore.MarkerManager + public static func check(registry: any MapConductorCore::MapServiceRegistry, provider: Swift::String, feature: Swift::String) -> Swift::Bool +} +public enum StyleMutation : Swift::Equatable, Swift::Sendable { + case setPaint(layerId: Swift::String, key: Swift::String, value: Swift::String, previous: Swift::String) + case setLayout(layerId: Swift::String, key: Swift::String, value: Swift::String, previous: Swift::String) + case setFilter(layerId: Swift::String, value: Swift::String, previous: Swift::String) + case setZoomRange(layerId: Swift::String, minZoom: Swift::Double?, maxZoom: Swift::Double?, previousMinZoom: Swift::Double?, previousMaxZoom: Swift::Double?) + case addLayer(layerId: Swift::String, layer: Swift::String, beforeId: Swift::String?) + case removeLayer(layerId: Swift::String) + public var layerId: Swift::String { + get + } + public var target: Swift::String { + get + } + public var propertyName: Swift::String { + get + } + public func reversed() -> MapConductorCore::StyleMutation? + public static func parseList(_ json: Swift::String) -> [MapConductorCore::StyleMutation] + public static func jsonText(of value: Any?) -> Swift::String + public static func == (a: MapConductorCore::StyleMutation, b: MapConductorCore::StyleMutation) -> Swift::Bool +} +public protocol VectorStyleMutationSupport : AnyObject { + func apply(_ mutations: [MapConductorCore::StyleMutation]) -> [MapConductorCore::StyleMutation] + func clear() +} +public enum VectorStyleMutationSupportKey : MapConductorCore::MapServiceKey { + public typealias Value = MapConductorCore::VectorStyleMutationSupport +} +final public class StyleMutationStore : MapConductorCore::VectorStyleMutationSupport, @unchecked Swift::Sendable { + public init(applyNow: @escaping ([MapConductorCore::StyleMutation]) -> [MapConductorCore::StyleMutation]) + final public func apply(_ mutations: [MapConductorCore::StyleMutation]) -> [MapConductorCore::StyleMutation] + final public func clear() + final public func onStyleLoaded() + final public var applied: [MapConductorCore::StyleMutation] { + get + } + @objc deinit +} +public protocol VectorStyleSupport : AnyObject { + func showStyle(url: Swift::String, attributionRules: [MapConductorCore::AttributionRule]) + func clearStyle() + var currentStyleUrl: Swift::String? { get } +} +extension MapConductorCore::VectorStyleSupport { + public var currentStyleUrl: Swift::String? { + get + } +} +public enum VectorStyleSupportKey : MapConductorCore::MapServiceKey { + public typealias Value = MapConductorCore::VectorStyleSupport +} +final public class VectorStyleAsDesign : MapConductorCore::VectorStyleSupport where State : MapConductorCore::MapViewStateProtocol { + public init(state: State, designId: @escaping (State.ActualMapDesignType) -> Swift::String, urlOf: ((State.ActualMapDesignType) -> Swift::String?)? = nil, designFor: @escaping (_ url: Swift::String, _ attributionRules: [MapConductorCore::AttributionRule]) -> State.ActualMapDesignType) + final public var currentStyleUrl: Swift::String? { + get + } + final public func showStyle(url: Swift::String, attributionRules: [MapConductorCore::AttributionRule]) + final public func clearStyle() + @objc deinit +} +extension MapConductorCore::MapViewHolderProtocol { + public func buildVisibleRegion(inset: CoreFoundation::CGFloat = 0, requireAllCorners: Swift::Bool = true) -> MapConductorCore::VisibleRegion? + public func buildVisibleRegion(size: CoreFoundation::CGSize, inset: CoreFoundation::CGFloat = 0, requireAllCorners: Swift::Bool = true) -> MapConductorCore::VisibleRegion? + public func viewportSizePx() -> CoreFoundation::CGSize? +} +open class AbstractMarkerController : MapConductorCore::OverlayControllerProtocol, MapConductorCore::SlottedOverlayController where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol { + public typealias StateType = MapConductorCore::MarkerState + public typealias EntityType = MapConductorCore::MarkerEntity + public typealias EventType = MapConductorCore::MarkerState + final public let markerManager: MapConductorCore::MarkerManager open var renderer: Renderer - final public let zIndex: Swift.Int - public var clickListener: ((MapConductorCore.MarkerState) -> Swift.Void)? - public var dragStartListener: MapConductorCore.OnMarkerEventHandler? - public var dragListener: MapConductorCore.OnMarkerEventHandler? - public var dragEndListener: MapConductorCore.OnMarkerEventHandler? - public var animateStartListener: MapConductorCore.OnMarkerEventHandler? - public var animateEndListener: MapConductorCore.OnMarkerEventHandler? - public init(markerManager: MapConductorCore.MarkerManager, renderer: Renderer, clickListener: ((MapConductorCore.MarkerState) -> Swift.Void)? = nil) - public func dispatchClick(state: MapConductorCore.MarkerState) - public func dispatchDragStart(state: MapConductorCore.MarkerState) - public func dispatchDrag(state: MapConductorCore.MarkerState) - public func dispatchDragEnd(state: MapConductorCore.MarkerState) - public func dispatchAnimateStart(state: MapConductorCore.MarkerState) - public func dispatchAnimateEnd(state: MapConductorCore.MarkerState) - open func setDraggingState(markerState: MapConductorCore.MarkerState, dragging: Swift.Bool) - public func isDragging(_ markerState: MapConductorCore.MarkerState) -> Swift.Bool - open func add(data: [MapConductorCore.MarkerState]) async - open func update(state: MapConductorCore.MarkerState) async + final public let zIndex: Swift::Int + public var clickListener: ((MapConductorCore::MarkerState) -> Swift::Void)? + public var dragStartListener: MapConductorCore::OnMarkerEventHandler? + public var dragListener: MapConductorCore::OnMarkerEventHandler? + public var dragEndListener: MapConductorCore::OnMarkerEventHandler? + public var animateStartListener: MapConductorCore::OnMarkerEventHandler? + public var animateEndListener: MapConductorCore::OnMarkerEventHandler? + public init(markerManager: MapConductorCore::MarkerManager, renderer: Renderer, clickListener: ((MapConductorCore::MarkerState) -> Swift::Void)? = nil) + public func dispatchClick(state: MapConductorCore::MarkerState) + public func dispatchDragStart(state: MapConductorCore::MarkerState) + public func dispatchDrag(state: MapConductorCore::MarkerState) + public func dispatchDragEnd(state: MapConductorCore::MarkerState) + public func dispatchAnimateStart(state: MapConductorCore::MarkerState) + public func dispatchAnimateEnd(state: MapConductorCore::MarkerState) + open func setDraggingState(markerState: MapConductorCore::MarkerState, dragging: Swift::Bool) + public func isDragging(_ markerState: MapConductorCore::MarkerState) -> Swift::Bool + open func add(data: [MapConductorCore::MarkerState]) async + open func update(state: MapConductorCore::MarkerState) async open func clear() async - open func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async - open func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.MarkerEntity? + open func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async + open func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::MarkerEntity? open func destroy() - public var kind: MapConductorCore.OverlayKind { + public var kind: MapConductorCore::OverlayKind { get } - public func hasId(_ id: Swift.String) -> Swift.Bool - public func resolveTap(position _: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.OverlayHit? + public func hasId(_ id: Swift::String) -> Swift::Bool + public func resolveTap(position _: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::OverlayHit? @objc deinit } -open class AbstractViewportStrategy : MapConductorCore.AbstractMarkerRenderingStrategy { - public init(semaphore: MapConductorCore.AsyncSemaphore = AsyncSemaphore(1), geocell: any MapConductorCore.HexGeocellProtocol = HexGeocell.defaultGeocell()) - override open func onAdd(data: [MapConductorCore.MarkerState], viewport: MapConductorCore.GeoRectBounds, renderer: Renderer) async -> Swift.Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol - override open func onUpdate(state: MapConductorCore.MarkerState, viewport: MapConductorCore.GeoRectBounds, renderer: Renderer) async -> Swift.Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol +open class AbstractViewportStrategy : MapConductorCore::AbstractMarkerRenderingStrategy { + public init(semaphore: MapConductorCore::AsyncSemaphore = AsyncSemaphore(1), geocell: any MapConductorCore::HexGeocellProtocol = HexGeocell.defaultGeocell()) + override open func onAdd(data: [MapConductorCore::MarkerState], viewport: MapConductorCore::GeoRectBounds, renderer: Renderer) async -> Swift::Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol + override open func onUpdate(state: MapConductorCore::MarkerState, viewport: MapConductorCore::GeoRectBounds, renderer: Renderer) async -> Swift::Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol override open func clear() @objc deinit } final public class AsyncSemaphore { - public init(_ permits: Swift.Int) + public init(_ permits: Swift::Int) final public func withPermit(_ operation: () async throws -> T) async rethrows -> T @objc deinit } -final public class ColorDefaultIcon : MapConductorCore.MarkerIconProtocol { - public static let defaultIconSize: CoreFoundation.CGFloat - public static let defaultStrokeWidth: CoreFoundation.CGFloat - public static let defaultFillColor: UIKit.UIColor - public static let defaultStrokeColor: UIKit.UIColor - public static let defaultLabelTextSize: CoreFoundation.CGFloat - public static let defaultLabelTextColor: UIKit.UIColor - public static let defaultLabelStrokeColor: UIKit.UIColor - public static let defaultLabelTypeFace: UIKit.UIFont - public static let defaultInfoAnchor: CoreFoundation.CGPoint - public static let defaultLabelPosition: Swift.String - final public let scale: CoreFoundation.CGFloat - final public let anchor: CoreFoundation.CGPoint - final public let iconSize: CoreFoundation.CGFloat - final public let infoAnchor: CoreFoundation.CGPoint - final public let debug: Swift.Bool - final public let labelPosition: Swift.String - public init(fillColor: UIKit.UIColor = defaultFillColor, strokeColor: UIKit.UIColor = defaultStrokeColor, strokeWidth: CoreFoundation.CGFloat = defaultStrokeWidth, scale: CoreFoundation.CGFloat = 1.0, label: Swift.String? = nil, labelTextColor: UIKit.UIColor? = defaultLabelTextColor, labelTextSize: CoreFoundation.CGFloat = defaultLabelTextSize, labelTypeFace: UIKit.UIFont = defaultLabelTypeFace, labelStrokeColor: UIKit.UIColor = defaultLabelStrokeColor, labelPosition: Swift.String = defaultLabelPosition, infoAnchor: CoreFoundation.CGPoint = defaultInfoAnchor, iconSize: CoreFoundation.CGFloat = defaultIconSize, debug: Swift.Bool = false) - final public func toBitmapIcon() -> MapConductorCore.BitmapIcon - final public func hashCode() -> Swift.Int - final public func copy(fillColor: UIKit.UIColor? = nil, strokeColor: UIKit.UIColor? = nil, strokeWidth: CoreFoundation.CGFloat? = nil, scale: CoreFoundation.CGFloat? = nil, label: Swift.String? = nil, labelTextColor: UIKit.UIColor? = nil, labelTextSize: CoreFoundation.CGFloat? = nil, labelTypeFace: UIKit.UIFont? = nil, labelStrokeColor: UIKit.UIColor? = nil, labelPosition: Swift.String? = nil, iconSize: CoreFoundation.CGFloat? = nil, debug: Swift.Bool? = nil) -> MapConductorCore.ColorDefaultIcon - @objc deinit -} -public typealias DefaultMarkerIcon = MapConductorCore.ColorDefaultIcon +final public class ColorDefaultIcon : MapConductorCore::MarkerIconProtocol { + public static let defaultIconSize: CoreFoundation::CGFloat + public static let defaultStrokeWidth: CoreFoundation::CGFloat + public static let defaultFillColor: UIKit::UIColor + public static let defaultStrokeColor: UIKit::UIColor + public static let defaultLabelTextSize: CoreFoundation::CGFloat + public static let defaultLabelTextColor: UIKit::UIColor + public static let defaultLabelStrokeColor: UIKit::UIColor + public static let defaultLabelTypeFace: UIKit::UIFont + public static let defaultInfoAnchor: CoreFoundation::CGPoint + public static let defaultLabelPosition: Swift::String + final public let scale: CoreFoundation::CGFloat + final public let anchor: CoreFoundation::CGPoint + final public let iconSize: CoreFoundation::CGFloat + final public let infoAnchor: CoreFoundation::CGPoint + final public let debug: Swift::Bool + final public let labelPosition: Swift::String + public init(fillColor: UIKit::UIColor = defaultFillColor, strokeColor: UIKit::UIColor = defaultStrokeColor, strokeWidth: CoreFoundation::CGFloat = defaultStrokeWidth, scale: CoreFoundation::CGFloat = 1.0, label: Swift::String? = nil, labelTextColor: UIKit::UIColor? = defaultLabelTextColor, labelTextSize: CoreFoundation::CGFloat = defaultLabelTextSize, labelTypeFace: UIKit::UIFont = defaultLabelTypeFace, labelStrokeColor: UIKit::UIColor = defaultLabelStrokeColor, labelPosition: Swift::String = defaultLabelPosition, infoAnchor: CoreFoundation::CGPoint = defaultInfoAnchor, iconSize: CoreFoundation::CGFloat = defaultIconSize, debug: Swift::Bool = false) + final public func toBitmapIcon() -> MapConductorCore::BitmapIcon + final public func hashCode() -> Swift::Int + final public func copy(fillColor: UIKit::UIColor? = nil, strokeColor: UIKit::UIColor? = nil, strokeWidth: CoreFoundation::CGFloat? = nil, scale: CoreFoundation::CGFloat? = nil, label: Swift::String? = nil, labelTextColor: UIKit::UIColor? = nil, labelTextSize: CoreFoundation::CGFloat? = nil, labelTypeFace: UIKit::UIFont? = nil, labelStrokeColor: UIKit::UIColor? = nil, labelPosition: Swift::String? = nil, iconSize: CoreFoundation::CGFloat? = nil, debug: Swift::Bool? = nil) -> MapConductorCore::ColorDefaultIcon + @objc deinit +} +public typealias DefaultMarkerIcon = MapConductorCore::ColorDefaultIcon public protocol MarkerDragSurface : AnyObject { - var isScrollEnabled: Swift.Bool { get set } - func geoPoint(atScreenPoint point: CoreFoundation.CGPoint) -> MapConductorCore.GeoPoint? + var isScrollEnabled: Swift::Bool { get set } + func geoPoint(atScreenPoint point: CoreFoundation::CGPoint) -> MapConductorCore::GeoPoint? } public protocol MarkerEventHostProtocol : AnyObject { - func markerId(atScreenPoint point: CoreFoundation.CGPoint) -> Swift.String? - func markerState(for id: Swift.String) -> MapConductorCore.MarkerState? - func handleTiledMarkerTap(atScreenPoint point: CoreFoundation.CGPoint) -> Swift.Bool - func dispatchClick(state: MapConductorCore.MarkerState) - func dispatchDragStart(state: MapConductorCore.MarkerState) - func dispatchDrag(state: MapConductorCore.MarkerState) - func dispatchDragEnd(state: MapConductorCore.MarkerState) - func onUpdateInfoBubble(_ markerId: Swift.String) -} -@_Concurrency.MainActor open class DefaultMarkerEventController { - @_Concurrency.MainActor public init(surface: (any MapConductorCore.MarkerDragSurface)?, host: any MapConductorCore.MarkerEventHostProtocol) - @_Concurrency.MainActor open func handleTap(at point: CoreFoundation.CGPoint) -> Swift.Bool - @_Concurrency.MainActor open func handleLongPress(state recognizerState: MapConductorCore.MarkerDragGestureState, at point: CoreFoundation.CGPoint) -> Swift.Bool - @_Concurrency.MainActor open func unbind() + func markerId(atScreenPoint point: CoreFoundation::CGPoint) -> Swift::String? + func markerState(for id: Swift::String) -> MapConductorCore::MarkerState? + func handleTiledMarkerTap(atScreenPoint point: CoreFoundation::CGPoint) -> Swift::Bool + func dispatchClick(state: MapConductorCore::MarkerState) + func dispatchDragStart(state: MapConductorCore::MarkerState) + func dispatchDrag(state: MapConductorCore::MarkerState) + func dispatchDragEnd(state: MapConductorCore::MarkerState) + func onUpdateInfoBubble(_ markerId: Swift::String) +} +@_Concurrency::MainActor open class DefaultMarkerEventController { + @_Concurrency::MainActor public init(surface: (any MapConductorCore::MarkerDragSurface)?, host: any MapConductorCore::MarkerEventHostProtocol) + @_Concurrency::MainActor open func handleTap(at point: CoreFoundation::CGPoint) -> Swift::Bool + @_Concurrency::MainActor open func handleLongPress(state recognizerState: MapConductorCore::MarkerDragGestureState, at point: CoreFoundation::CGPoint) -> Swift::Bool + @_Concurrency::MainActor open func unbind() @objc deinit } public enum MarkerDragGestureState { @@ -1574,1440 +1710,1405 @@ public enum MarkerDragGestureState { case ended case cancelled case other - public static func == (a: MapConductorCore.MarkerDragGestureState, b: MapConductorCore.MarkerDragGestureState) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { - get - } -} -final public class ImageDefaultIcon : MapConductorCore.MarkerIconProtocol { - public static let defaultIconSize: CoreFoundation.CGFloat - public static let defaultStrokeWidth: CoreFoundation.CGFloat - public static let defaultStrokeColor: UIKit.UIColor - public static let defaultLabelTextSize: CoreFoundation.CGFloat - public static let defaultLabelTextColor: UIKit.UIColor - public static let defaultLabelStrokeColor: UIKit.UIColor - public static let defaultLabelTypeFace: UIKit.UIFont - public static let defaultInfoAnchor: CoreFoundation.CGPoint - public static let defaultLabelPosition: Swift.String - public static let defaultAnchor: CoreFoundation.CGPoint - final public let scale: CoreFoundation.CGFloat - final public let anchor: CoreFoundation.CGPoint - final public let iconSize: CoreFoundation.CGFloat - final public let infoAnchor: CoreFoundation.CGPoint - final public let debug: Swift.Bool - final public let strokeColor: UIKit.UIColor - final public let strokeWidth: CoreFoundation.CGFloat - final public let label: Swift.String? - final public let labelTextColor: UIKit.UIColor? - final public let labelTextSize: CoreFoundation.CGFloat - final public let labelTypeFace: UIKit.UIFont - final public let labelStrokeColor: UIKit.UIColor - final public let labelPosition: Swift.String - public init(backgroundImage: UIKit.UIImage, strokeColor: UIKit.UIColor = defaultStrokeColor, strokeWidth: CoreFoundation.CGFloat = defaultStrokeWidth, scale: CoreFoundation.CGFloat = 1.0, label: Swift.String? = nil, labelTextColor: UIKit.UIColor? = defaultLabelTextColor, labelTextSize: CoreFoundation.CGFloat = defaultLabelTextSize, labelTypeFace: UIKit.UIFont = defaultLabelTypeFace, labelStrokeColor: UIKit.UIColor = defaultLabelStrokeColor, labelPosition: Swift.String = defaultLabelPosition, infoAnchor: CoreFoundation.CGPoint = defaultInfoAnchor, iconSize: CoreFoundation.CGFloat = defaultIconSize, debug: Swift.Bool = false) - final public func toBitmapIcon() -> MapConductorCore.BitmapIcon - final public func hashCode() -> Swift.Int - final public func copy(backgroundImage: UIKit.UIImage? = nil, strokeColor: UIKit.UIColor? = nil, strokeWidth: CoreFoundation.CGFloat? = nil, scale: CoreFoundation.CGFloat? = nil, label: Swift.String? = nil, labelTextColor: UIKit.UIColor? = nil, labelTextSize: CoreFoundation.CGFloat? = nil, labelTypeFace: UIKit.UIFont? = nil, labelStrokeColor: UIKit.UIColor? = nil, labelPosition: Swift.String? = nil, iconSize: CoreFoundation.CGFloat? = nil, debug: Swift.Bool? = nil) -> MapConductorCore.ImageDefaultIcon - @objc deinit -} -final public class ImageIcon : MapConductorCore.MarkerIconProtocol { - public static let defaultIconSize: CoreFoundation.CGFloat - public static let defaultAnchor: CoreFoundation.CGPoint - public static let defaultInfoAnchor: CoreFoundation.CGPoint - final public let iconSize: CoreFoundation.CGFloat - final public let scale: CoreFoundation.CGFloat - final public let anchor: CoreFoundation.CGPoint - final public let infoAnchor: CoreFoundation.CGPoint - final public let debug: Swift.Bool - public init(image: UIKit.UIImage, iconSize: CoreFoundation.CGFloat = defaultIconSize, scale: CoreFoundation.CGFloat = 1.0, anchor: CoreFoundation.CGPoint = defaultAnchor, infoAnchor: CoreFoundation.CGPoint = defaultInfoAnchor, debug: Swift.Bool = false) - final public func toBitmapIcon() -> MapConductorCore.BitmapIcon - final public func hashCode() -> Swift.Int - final public func copy(image: UIKit.UIImage? = nil, iconSize: CoreFoundation.CGFloat? = nil, scale: CoreFoundation.CGFloat? = nil, anchor: CoreFoundation.CGPoint? = nil, infoAnchor: CoreFoundation.CGPoint? = nil, debug: Swift.Bool? = nil) -> MapConductorCore.ImageIcon + public static func == (a: MapConductorCore::MarkerDragGestureState, b: MapConductorCore::MarkerDragGestureState) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { + get + } +} +final public class ImageDefaultIcon : MapConductorCore::MarkerIconProtocol { + public static let defaultIconSize: CoreFoundation::CGFloat + public static let defaultStrokeWidth: CoreFoundation::CGFloat + public static let defaultStrokeColor: UIKit::UIColor + public static let defaultLabelTextSize: CoreFoundation::CGFloat + public static let defaultLabelTextColor: UIKit::UIColor + public static let defaultLabelStrokeColor: UIKit::UIColor + public static let defaultLabelTypeFace: UIKit::UIFont + public static let defaultInfoAnchor: CoreFoundation::CGPoint + public static let defaultLabelPosition: Swift::String + public static let defaultAnchor: CoreFoundation::CGPoint + final public let scale: CoreFoundation::CGFloat + final public let anchor: CoreFoundation::CGPoint + final public let iconSize: CoreFoundation::CGFloat + final public let infoAnchor: CoreFoundation::CGPoint + final public let debug: Swift::Bool + final public let strokeColor: UIKit::UIColor + final public let strokeWidth: CoreFoundation::CGFloat + final public let label: Swift::String? + final public let labelTextColor: UIKit::UIColor? + final public let labelTextSize: CoreFoundation::CGFloat + final public let labelTypeFace: UIKit::UIFont + final public let labelStrokeColor: UIKit::UIColor + final public let labelPosition: Swift::String + public init(backgroundImage: UIKit::UIImage, strokeColor: UIKit::UIColor = defaultStrokeColor, strokeWidth: CoreFoundation::CGFloat = defaultStrokeWidth, scale: CoreFoundation::CGFloat = 1.0, label: Swift::String? = nil, labelTextColor: UIKit::UIColor? = defaultLabelTextColor, labelTextSize: CoreFoundation::CGFloat = defaultLabelTextSize, labelTypeFace: UIKit::UIFont = defaultLabelTypeFace, labelStrokeColor: UIKit::UIColor = defaultLabelStrokeColor, labelPosition: Swift::String = defaultLabelPosition, infoAnchor: CoreFoundation::CGPoint = defaultInfoAnchor, iconSize: CoreFoundation::CGFloat = defaultIconSize, debug: Swift::Bool = false) + final public func toBitmapIcon() -> MapConductorCore::BitmapIcon + final public func hashCode() -> Swift::Int + final public func copy(backgroundImage: UIKit::UIImage? = nil, strokeColor: UIKit::UIColor? = nil, strokeWidth: CoreFoundation::CGFloat? = nil, scale: CoreFoundation::CGFloat? = nil, label: Swift::String? = nil, labelTextColor: UIKit::UIColor? = nil, labelTextSize: CoreFoundation::CGFloat? = nil, labelTypeFace: UIKit::UIFont? = nil, labelStrokeColor: UIKit::UIColor? = nil, labelPosition: Swift::String? = nil, iconSize: CoreFoundation::CGFloat? = nil, debug: Swift::Bool? = nil) -> MapConductorCore::ImageDefaultIcon + @objc deinit +} +final public class ImageIcon : MapConductorCore::MarkerIconProtocol { + public static let defaultIconSize: CoreFoundation::CGFloat + public static let defaultAnchor: CoreFoundation::CGPoint + public static let defaultInfoAnchor: CoreFoundation::CGPoint + final public let iconSize: CoreFoundation::CGFloat + final public let scale: CoreFoundation::CGFloat + final public let anchor: CoreFoundation::CGPoint + final public let infoAnchor: CoreFoundation::CGPoint + final public let debug: Swift::Bool + public init(image: UIKit::UIImage, iconSize: CoreFoundation::CGFloat = defaultIconSize, scale: CoreFoundation::CGFloat = 1.0, anchor: CoreFoundation::CGPoint = defaultAnchor, infoAnchor: CoreFoundation::CGPoint = defaultInfoAnchor, debug: Swift::Bool = false) + final public func toBitmapIcon() -> MapConductorCore::BitmapIcon + final public func hashCode() -> Swift::Int + final public func copy(image: UIKit::UIImage? = nil, iconSize: CoreFoundation::CGFloat? = nil, scale: CoreFoundation::CGFloat? = nil, anchor: CoreFoundation::CGPoint? = nil, infoAnchor: CoreFoundation::CGPoint? = nil, debug: Swift::Bool? = nil) -> MapConductorCore::ImageIcon @objc deinit } public struct MarkerAnimationOverlayEntry { - public let id: Swift.String - public let state: MapConductorCore.MarkerState - public let icon: MapConductorCore.BitmapIcon - public let animation: MapConductorCore.MarkerAnimation - public let duration: CoreFoundation.CFTimeInterval - public let onFinished: () -> Swift.Void - public init(id: Swift.String, state: MapConductorCore.MarkerState, icon: MapConductorCore.BitmapIcon, animation: MapConductorCore.MarkerAnimation, duration: CoreFoundation.CFTimeInterval, onFinished: @escaping () -> Swift.Void) + public let id: Swift::String + public let state: MapConductorCore::MarkerState + public let icon: MapConductorCore::BitmapIcon + public let animation: MapConductorCore::MarkerAnimation + public let duration: CoreFoundation::CFTimeInterval + public let onFinished: () -> Swift::Void + public init(id: Swift::String, state: MapConductorCore::MarkerState, icon: MapConductorCore::BitmapIcon, animation: MapConductorCore::MarkerAnimation, duration: CoreFoundation::CFTimeInterval, onFinished: @escaping () -> Swift::Void) } -@_Concurrency.MainActor public protocol MarkerAnimationOverlayHost : AnyObject { - @_Concurrency.MainActor func start(_ entry: MapConductorCore.MarkerAnimationOverlayEntry) +@_Concurrency::MainActor public protocol MarkerAnimationOverlayHost : AnyObject { + @_Concurrency::MainActor func start(_ entry: MapConductorCore::MarkerAnimationOverlayEntry) } -@_Concurrency.MainActor final public class MarkerAnimationOverlayCoordinator : MapConductorCore.MarkerAnimationOverlayHost { - public typealias Projection = (any MapConductorCore.GeoPointProtocol) -> CoreFoundation.CGPoint? - @_Concurrency.MainActor public init(container: UIKit.UIView, project: @escaping MapConductorCore.MarkerAnimationOverlayCoordinator.Projection, projectionGate: @escaping () -> Swift.Bool = { true }) - @_Concurrency.MainActor final public func start(_ entry: MapConductorCore.MarkerAnimationOverlayEntry) - @_Concurrency.MainActor final public func unbind() +@_Concurrency::MainActor final public class MarkerAnimationOverlayCoordinator : MapConductorCore::MarkerAnimationOverlayHost { + public typealias Projection = (any MapConductorCore::GeoPointProtocol) -> CoreFoundation::CGPoint? + @_Concurrency::MainActor public init(container: UIKit::UIView, project: @escaping MapConductorCore::MarkerAnimationOverlayCoordinator.MapConductorCore::Projection, projectionGate: @escaping () -> Swift::Bool = { true }) + @_Concurrency::MainActor final public func start(_ entry: MapConductorCore::MarkerAnimationOverlayEntry) + @_Concurrency::MainActor final public func unbind() @objc deinit } final public class MarkerAnimationRunner { - public init(duration: CoreFoundation.CFTimeInterval, pointAtProgress: @escaping (CoreFoundation.CGFloat) -> MapConductorCore.GeoPoint, onUpdate: @escaping (MapConductorCore.GeoPoint) -> Swift.Void, onCompletion: @escaping () -> Swift.Void) - convenience public init(duration: CoreFoundation.CFTimeInterval, pathPoints: [MapConductorCore.GeoPoint], onUpdate: @escaping (MapConductorCore.GeoPoint) -> Swift.Void, onCompletion: @escaping () -> Swift.Void) + public init(duration: CoreFoundation::CFTimeInterval, pointAtProgress: @escaping (CoreFoundation::CGFloat) -> MapConductorCore::GeoPoint, onUpdate: @escaping (MapConductorCore::GeoPoint) -> Swift::Void, onCompletion: @escaping () -> Swift::Void) + convenience public init(duration: CoreFoundation::CFTimeInterval, pathPoints: [MapConductorCore::GeoPoint], onUpdate: @escaping (MapConductorCore::GeoPoint) -> Swift::Void, onCompletion: @escaping () -> Swift::Void) final public func start() final public func stop() - public static func makeLinearPath(start: MapConductorCore.GeoPoint, target: MapConductorCore.GeoPoint) -> [MapConductorCore.GeoPoint] - public static func makeEaseOutBounceSamples(start: MapConductorCore.GeoPoint, target: MapConductorCore.GeoPoint, samples: Swift.Int = 30) -> [MapConductorCore.GeoPoint] + public static func makeLinearPath(start: MapConductorCore::GeoPoint, target: MapConductorCore::GeoPoint) -> [MapConductorCore::GeoPoint] + public static func makeEaseOutBounceSamples(start: MapConductorCore::GeoPoint, target: MapConductorCore::GeoPoint, samples: Swift::Int = 30) -> [MapConductorCore::GeoPoint] @objc deinit } public protocol MarkerEntityProtocol { associatedtype ActualMarker var marker: Self.ActualMarker? { get set } - var state: MapConductorCore.MarkerState { get } - var fingerPrint: MapConductorCore.MarkerFingerPrint { get } - var visible: Swift.Bool { get set } - var isRendered: Swift.Bool { get set } - var tiling: Swift.Bool { get set } + var state: MapConductorCore::MarkerState { get } + var fingerPrint: MapConductorCore::MarkerFingerPrint { get } + var visible: Swift::Bool { get set } + var isRendered: Swift::Bool { get set } + var tiling: Swift::Bool { get set } } -final public class MarkerEntity : MapConductorCore.MarkerEntityProtocol { +final public class MarkerEntity : MapConductorCore::MarkerEntityProtocol { final public var marker: ActualMarker? - final public let state: MapConductorCore.MarkerState - final public let fingerPrint: MapConductorCore.MarkerFingerPrint - final public var visible: Swift.Bool - final public var isRendered: Swift.Bool - final public var tiling: Swift.Bool - public init(marker: ActualMarker?, state: MapConductorCore.MarkerState, visible: Swift.Bool = true, isRendered: Swift.Bool = false, tiling: Swift.Bool = false) + final public let state: MapConductorCore::MarkerState + final public let fingerPrint: MapConductorCore::MarkerFingerPrint + final public var visible: Swift::Bool + final public var isRendered: Swift::Bool + final public var tiling: Swift::Bool + public init(marker: ActualMarker?, state: MapConductorCore::MarkerState, visible: Swift::Bool = true, isRendered: Swift::Bool = false, tiling: Swift::Bool = false) @objc deinit } public enum MarkerHitTest { - public static func hitsIcon(touchScreen: CoreFoundation.CGPoint, markerScreen: CoreFoundation.CGPoint, state: MapConductorCore.MarkerState, defaultIcon: any MapConductorCore.MarkerIconProtocol = DefaultMarkerIcon()) -> Swift.Bool + public static func hitsIcon(touchScreen: CoreFoundation::CGPoint, markerScreen: CoreFoundation::CGPoint, state: MapConductorCore::MarkerState, defaultIcon: any MapConductorCore::MarkerIconProtocol = DefaultMarkerIcon()) -> Swift::Bool } public protocol MarkerIconProtocol { - var scale: CoreFoundation.CGFloat { get } - var anchor: CoreFoundation.CGPoint { get } - var iconSize: CoreFoundation.CGFloat { get } - var infoAnchor: CoreFoundation.CGPoint { get } - var debug: Swift.Bool { get } - func toBitmapIcon() -> MapConductorCore.BitmapIcon - func hashCode() -> Swift.Int -} -public struct BitmapIcon : MapConductorCore.MarkerIconProtocol, Swift.Equatable, Swift.Hashable { - public let bitmap: UIKit.UIImage - public let anchor: CoreFoundation.CGPoint - public let size: CoreFoundation.CGSize - public let scale: CoreFoundation.CGFloat - public let iconSize: CoreFoundation.CGFloat - public let infoAnchor: CoreFoundation.CGPoint - public let debug: Swift.Bool - public init(bitmap: UIKit.UIImage, anchor: CoreFoundation.CGPoint = CGPoint(x: 0.5, y: 1.0), size: CoreFoundation.CGSize? = nil, scale: CoreFoundation.CGFloat = 1.0, iconSize: CoreFoundation.CGFloat? = nil, infoAnchor: CoreFoundation.CGPoint = CGPoint(x: 0.5, y: 0.0), debug: Swift.Bool = false) - public func toBitmapIcon() -> MapConductorCore.BitmapIcon - public func hashCode() -> Swift.Int - public func toByteArray() -> [Swift.UInt8] - public static func == (a: MapConductorCore.BitmapIcon, b: MapConductorCore.BitmapIcon) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { + var scale: CoreFoundation::CGFloat { get } + var anchor: CoreFoundation::CGPoint { get } + var iconSize: CoreFoundation::CGFloat { get } + var infoAnchor: CoreFoundation::CGPoint { get } + var debug: Swift::Bool { get } + func toBitmapIcon() -> MapConductorCore::BitmapIcon + func hashCode() -> Swift::Int +} +public struct BitmapIcon : MapConductorCore::MarkerIconProtocol, Swift::Equatable, Swift::Hashable { + public let bitmap: UIKit::UIImage + public let anchor: CoreFoundation::CGPoint + public let size: CoreFoundation::CGSize + public let scale: CoreFoundation::CGFloat + public let iconSize: CoreFoundation::CGFloat + public let infoAnchor: CoreFoundation::CGPoint + public let debug: Swift::Bool + public init(bitmap: UIKit::UIImage, anchor: CoreFoundation::CGPoint = CGPoint(x: 0.5, y: 1.0), size: CoreFoundation::CGSize? = nil, scale: CoreFoundation::CGFloat = 1.0, iconSize: CoreFoundation::CGFloat? = nil, infoAnchor: CoreFoundation::CGPoint = CGPoint(x: 0.5, y: 0.0), debug: Swift::Bool = false) + public func toBitmapIcon() -> MapConductorCore::BitmapIcon + public func hashCode() -> Swift::Int + public func toByteArray() -> [Swift::UInt8] + public static func == (a: MapConductorCore::BitmapIcon, b: MapConductorCore::BitmapIcon) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public enum MarkerIngestionEngine { public struct Result { - public let tiledDataChanged: Swift.Bool - public let hasTiledMarkers: Swift.Bool + public let tiledDataChanged: Swift::Bool + public let hasTiledMarkers: Swift::Bool } - @_Concurrency.MainActor public static func ingest(data: [MapConductorCore.MarkerState], markerManager: MapConductorCore.MarkerManager, renderer: Renderer, defaultMarkerIcon: MapConductorCore.BitmapIcon, tilingEnabled: Swift.Bool, tiledMarkerIds: inout Swift.Set, shouldTile: (MapConductorCore.MarkerState) -> Swift.Bool) async -> MapConductorCore.MarkerIngestionEngine.Result where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol + @_Concurrency::MainActor public static func ingest(data: [MapConductorCore::MarkerState], markerManager: MapConductorCore::MarkerManager, renderer: Renderer, defaultMarkerIcon: MapConductorCore::BitmapIcon, tilingEnabled: Swift::Bool, tiledMarkerIds: inout Swift::Set, shouldTile: (MapConductorCore::MarkerState) -> Swift::Bool) async -> MapConductorCore::MarkerIngestionEngine.MapConductorCore::Result where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol } -public struct MarkerManagerStats : Swift.Sendable, Swift.Hashable { - public let entityCount: Swift.Int - public let hasSpatialIndex: Swift.Bool - public let spatialIndexInitialized: Swift.Bool - public let estimatedMemoryKB: Swift.Int - public static func == (a: MapConductorCore.MarkerManagerStats, b: MapConductorCore.MarkerManagerStats) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public struct MarkerManagerStats : Swift::Sendable, Swift::Hashable { + public let entityCount: Swift::Int + public let hasSpatialIndex: Swift::Bool + public let spatialIndexInitialized: Swift::Bool + public let estimatedMemoryKB: Swift::Int + public static func == (a: MapConductorCore::MarkerManagerStats, b: MapConductorCore::MarkerManagerStats) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } final public class MarkerManager { - final public let minMarkerCount: Swift.Int - public init(geocell: any MapConductorCore.HexGeocellProtocol = HexGeocell.defaultGeocell(), minMarkerCount: Swift.Int = 2000) - final public func getEntity(_ id: Swift.String) -> MapConductorCore.MarkerEntity? - final public func hasEntity(_ id: Swift.String) -> Swift.Bool + final public let minMarkerCount: Swift::Int + public init(geocell: any MapConductorCore::HexGeocellProtocol = HexGeocell.defaultGeocell(), minMarkerCount: Swift::Int = 2000) + final public func getEntity(_ id: Swift::String) -> MapConductorCore::MarkerEntity? + final public func hasEntity(_ id: Swift::String) -> Swift::Bool @discardableResult - final public func removeEntity(_ id: Swift.String) -> MapConductorCore.MarkerEntity? - final public func registerEntity(_ entity: MapConductorCore.MarkerEntity) - final public func updateEntity(_ entity: MapConductorCore.MarkerEntity) - final public func metersPerPixel(position: any MapConductorCore.GeoPointProtocol, zoom: Swift.Double, pixels: Swift.Double, tileSize: Swift.Int = 256) -> Swift.Double - final public func findNearest(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.MarkerEntity? - final public func findByIdPrefix(_ prefix: Swift.String) -> [MapConductorCore.HexCell] - final public func allEntities() -> [MapConductorCore.MarkerEntity] - final public func findMarkersInBounds(_ bounds: MapConductorCore.GeoRectBounds) -> [MapConductorCore.MarkerEntity] - final public func getMemoryStats() -> MapConductorCore.MarkerManagerStats + final public func removeEntity(_ id: Swift::String) -> MapConductorCore::MarkerEntity? + final public func registerEntity(_ entity: MapConductorCore::MarkerEntity) + final public func updateEntity(_ entity: MapConductorCore::MarkerEntity) + final public func metersPerPixel(position: any MapConductorCore::GeoPointProtocol, zoom: Swift::Double, pixels: Swift::Double, tileSize: Swift::Int = 256) -> Swift::Double + final public func findNearest(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::MarkerEntity? + final public func findByIdPrefix(_ prefix: Swift::String) -> [MapConductorCore::HexCell] + final public func allEntities() -> [MapConductorCore::MarkerEntity] + final public func findMarkersInBounds(_ bounds: MapConductorCore::GeoRectBounds) -> [MapConductorCore::MarkerEntity] + final public func findMarkersInBounds(_ bounds: MapConductorCore::GeoRectBounds, minSeparationDegrees: Swift::Double) -> [MapConductorCore::MarkerEntity] + final public func getMemoryStats() -> MapConductorCore::MarkerManagerStats final public func clear() final public func destroy() - final public var isDestroyed: Swift.Bool { + final public var isDestroyed: Swift::Bool { get } - public static func defaultManager() -> MapConductorCore.MarkerManager - public static func defaultManager(geocell: any MapConductorCore.HexGeocellProtocol) -> MapConductorCore.MarkerManager - public static func defaultManager(geocell: (any MapConductorCore.HexGeocellProtocol)? = nil, minMarkerCount: Swift.Int = 2000) -> MapConductorCore.MarkerManager + public static func defaultManager() -> MapConductorCore::MarkerManager + public static func defaultManager(geocell: any MapConductorCore::HexGeocellProtocol) -> MapConductorCore::MarkerManager + public static func defaultManager(geocell: (any MapConductorCore::HexGeocellProtocol)? = nil, minMarkerCount: Swift::Int = 2000) -> MapConductorCore::MarkerManager @objc deinit } -@_Concurrency.MainActor public protocol MarkerOverlayRendererProtocol { +@_Concurrency::MainActor public protocol MarkerOverlayRendererProtocol { associatedtype ActualMarker - @_Concurrency.MainActor var animateStartListener: MapConductorCore.OnMarkerEventHandler? { get set } - @_Concurrency.MainActor var animateEndListener: MapConductorCore.OnMarkerEventHandler? { get set } - @_Concurrency.MainActor func onAdd(data: [MapConductorCore.MarkerOverlayAddParams]) async -> [Self.ActualMarker?] - @_Concurrency.MainActor func onChange(data: [MapConductorCore.MarkerOverlayChangeParams]) async -> [Self.ActualMarker?] - @_Concurrency.MainActor func onRemove(data: [MapConductorCore.MarkerEntity]) async - @_Concurrency.MainActor func onAnimate(entity: MapConductorCore.MarkerEntity) async - @_Concurrency.MainActor func onPostProcess() async - @_Concurrency.MainActor func unbind() -} -extension MapConductorCore.MarkerOverlayRendererProtocol { - @_Concurrency.MainActor public func unbind() + @_Concurrency::MainActor var animateStartListener: MapConductorCore::OnMarkerEventHandler? { get set } + @_Concurrency::MainActor var animateEndListener: MapConductorCore::OnMarkerEventHandler? { get set } + @_Concurrency::MainActor func onAdd(data: [MapConductorCore::MarkerOverlayAddParams]) async -> [Self.ActualMarker?] + @_Concurrency::MainActor func onChange(data: [MapConductorCore::MarkerOverlayChangeParams]) async -> [Self.ActualMarker?] + @_Concurrency::MainActor func onRemove(data: [MapConductorCore::MarkerEntity]) async + @_Concurrency::MainActor func onAnimate(entity: MapConductorCore::MarkerEntity) async + @_Concurrency::MainActor func onPostProcess() async + @_Concurrency::MainActor func unbind() +} +extension MapConductorCore::MarkerOverlayRendererProtocol { + @_Concurrency::MainActor public func unbind() } public struct MarkerOverlayAddParams { - public let state: MapConductorCore.MarkerState - public let bitmapIcon: MapConductorCore.BitmapIcon - public init(state: MapConductorCore.MarkerState, bitmapIcon: MapConductorCore.BitmapIcon) + public let state: MapConductorCore::MarkerState + public let bitmapIcon: MapConductorCore::BitmapIcon + public init(state: MapConductorCore::MarkerState, bitmapIcon: MapConductorCore::BitmapIcon) } public struct MarkerOverlayChangeParams { - public let current: MapConductorCore.MarkerEntity - public let bitmapIcon: MapConductorCore.BitmapIcon - public let prev: MapConductorCore.MarkerEntity - public init(current: MapConductorCore.MarkerEntity, bitmapIcon: MapConductorCore.BitmapIcon, prev: MapConductorCore.MarkerEntity) + public let current: MapConductorCore::MarkerEntity + public let bitmapIcon: MapConductorCore::BitmapIcon + public let prev: MapConductorCore::MarkerEntity + public init(current: MapConductorCore::MarkerEntity, bitmapIcon: MapConductorCore::BitmapIcon, prev: MapConductorCore::MarkerEntity) } public protocol MarkerRenderingStrategyProtocol { associatedtype ActualMarker - var markerManager: MapConductorCore.MarkerManager { get } + var markerManager: MapConductorCore::MarkerManager { get } func clear() - func onAdd(data: [MapConductorCore.MarkerState], viewport: MapConductorCore.GeoRectBounds, renderer: Renderer) async -> Swift.Bool where Renderer : MapConductorCore.MarkerOverlayRendererProtocol, Self.ActualMarker == Renderer.ActualMarker - func onUpdate(state: MapConductorCore.MarkerState, viewport: MapConductorCore.GeoRectBounds, renderer: Renderer) async -> Swift.Bool where Renderer : MapConductorCore.MarkerOverlayRendererProtocol, Self.ActualMarker == Renderer.ActualMarker - func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition, renderer: Renderer) async where Renderer : MapConductorCore.MarkerOverlayRendererProtocol, Self.ActualMarker == Renderer.ActualMarker + func onAdd(data: [MapConductorCore::MarkerState], viewport: MapConductorCore::GeoRectBounds, renderer: Renderer) async -> Swift::Bool where Renderer : MapConductorCore::MarkerOverlayRendererProtocol, Self.ActualMarker == Renderer.ActualMarker + func onUpdate(state: MapConductorCore::MarkerState, viewport: MapConductorCore::GeoRectBounds, renderer: Renderer) async -> Swift::Bool where Renderer : MapConductorCore::MarkerOverlayRendererProtocol, Self.ActualMarker == Renderer.ActualMarker + func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition, renderer: Renderer) async where Renderer : MapConductorCore::MarkerOverlayRendererProtocol, Self.ActualMarker == Renderer.ActualMarker } -public struct AnyMarkerRenderingStrategy : MapConductorCore.MarkerRenderingStrategyProtocol { - public let markerManager: MapConductorCore.MarkerManager - public init(_ strategy: Strategy) where ActualMarker == Strategy.ActualMarker, Strategy : MapConductorCore.MarkerRenderingStrategyProtocol +public struct AnyMarkerRenderingStrategy : MapConductorCore::MarkerRenderingStrategyProtocol { + public let markerManager: MapConductorCore::MarkerManager + public init(_ strategy: Strategy) where ActualMarker == Strategy.ActualMarker, Strategy : MapConductorCore::MarkerRenderingStrategyProtocol public func clear() - public func onAdd(data: [MapConductorCore.MarkerState], viewport: MapConductorCore.GeoRectBounds, renderer: Renderer) async -> Swift.Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol - public func onUpdate(state: MapConductorCore.MarkerState, viewport: MapConductorCore.GeoRectBounds, renderer: Renderer) async -> Swift.Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol - public func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition, renderer: Renderer) async where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol + public func onAdd(data: [MapConductorCore::MarkerState], viewport: MapConductorCore::GeoRectBounds, renderer: Renderer) async -> Swift::Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol + public func onUpdate(state: MapConductorCore::MarkerState, viewport: MapConductorCore::GeoRectBounds, renderer: Renderer) async -> Swift::Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol + public func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition, renderer: Renderer) async where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol } -@_Concurrency.MainActor public struct AnyMarkerOverlayRenderer : MapConductorCore.MarkerOverlayRendererProtocol { - @_Concurrency.MainActor public var animateStartListener: MapConductorCore.OnMarkerEventHandler? { +@_Concurrency::MainActor public struct AnyMarkerOverlayRenderer : MapConductorCore::MarkerOverlayRendererProtocol { + @_Concurrency::MainActor public var animateStartListener: MapConductorCore::OnMarkerEventHandler? { get set } - @_Concurrency.MainActor public var animateEndListener: MapConductorCore.OnMarkerEventHandler? { + @_Concurrency::MainActor public var animateEndListener: MapConductorCore::OnMarkerEventHandler? { get set } - @_Concurrency.MainActor public init(_ renderer: Renderer) where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol - @_Concurrency.MainActor public func onAdd(data: [MapConductorCore.MarkerOverlayAddParams]) async -> [ActualMarker?] - @_Concurrency.MainActor public func onChange(data: [MapConductorCore.MarkerOverlayChangeParams]) async -> [ActualMarker?] - @_Concurrency.MainActor public func onRemove(data: [MapConductorCore.MarkerEntity]) async - @_Concurrency.MainActor public func onAnimate(entity: MapConductorCore.MarkerEntity) async - @_Concurrency.MainActor public func onPostProcess() async + @_Concurrency::MainActor public init(_ renderer: Renderer) where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol + @_Concurrency::MainActor public func onAdd(data: [MapConductorCore::MarkerOverlayAddParams]) async -> [ActualMarker?] + @_Concurrency::MainActor public func onChange(data: [MapConductorCore::MarkerOverlayChangeParams]) async -> [ActualMarker?] + @_Concurrency::MainActor public func onRemove(data: [MapConductorCore::MarkerEntity]) async + @_Concurrency::MainActor public func onAnimate(entity: MapConductorCore::MarkerEntity) async + @_Concurrency::MainActor public func onPostProcess() async } -open class AbstractMarkerRenderingStrategy : MapConductorCore.MarkerRenderingStrategyProtocol { - final public let markerManager: MapConductorCore.MarkerManager - final public let semaphore: MapConductorCore.AsyncSemaphore - final public let defaultMarkerIcon: MapConductorCore.BitmapIcon - public init(markerManager: MapConductorCore.MarkerManager = MarkerManager(), semaphore: MapConductorCore.AsyncSemaphore = AsyncSemaphore(1)) +open class AbstractMarkerRenderingStrategy : MapConductorCore::MarkerRenderingStrategyProtocol { + final public let markerManager: MapConductorCore::MarkerManager + final public let semaphore: MapConductorCore::AsyncSemaphore + final public let defaultMarkerIcon: MapConductorCore::BitmapIcon + public init(markerManager: MapConductorCore::MarkerManager = MarkerManager(), semaphore: MapConductorCore::AsyncSemaphore = AsyncSemaphore(1)) open func clear() - open func onAdd(data: [MapConductorCore.MarkerState], viewport: MapConductorCore.GeoRectBounds, renderer: Renderer) async -> Swift.Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol - open func onUpdate(state: MapConductorCore.MarkerState, viewport: MapConductorCore.GeoRectBounds, renderer: Renderer) async -> Swift.Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol - open func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition, renderer: Renderer) async where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol + open func onAdd(data: [MapConductorCore::MarkerState], viewport: MapConductorCore::GeoRectBounds, renderer: Renderer) async -> Swift::Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol + open func onUpdate(state: MapConductorCore::MarkerState, viewport: MapConductorCore::GeoRectBounds, renderer: Renderer) async -> Swift::Bool where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol + open func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition, renderer: Renderer) async where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol @objc deinit } -@_Concurrency.MainActor public protocol MarkerRenderingSupport : AnyObject { +@_Concurrency::MainActor public protocol MarkerRenderingSupport : AnyObject { @discardableResult - @_Concurrency.MainActor func connect(strategy: Any, markers: [MapConductorCore.MarkerState]) -> Swift.Bool - @_Concurrency.MainActor func syncMarkers(_ markers: [MapConductorCore.MarkerState]) - @_Concurrency.MainActor func disconnect() - @_Concurrency.MainActor func beginContentPass() - @_Concurrency.MainActor func endContentPass() + @_Concurrency::MainActor func connect(strategy: Any, markers: [MapConductorCore::MarkerState]) -> Swift::Bool + @_Concurrency::MainActor func syncMarkers(_ markers: [MapConductorCore::MarkerState]) + @_Concurrency::MainActor func disconnect() + @_Concurrency::MainActor func beginContentPass() + @_Concurrency::MainActor func endContentPass() } -public enum MarkerRenderingSupportKey : MapConductorCore.MapServiceKey { - public typealias Value = any MapConductorCore.MarkerRenderingSupport +public enum MarkerRenderingSupportKey : MapConductorCore::MapServiceKey { + public typealias Value = any MapConductorCore::MarkerRenderingSupport } public enum MarkerAnimation { case Drop case Bounce - public static func == (a: MapConductorCore.MarkerAnimation, b: MapConductorCore.MarkerAnimation) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { + public static func == (a: MapConductorCore::MarkerAnimation, b: MapConductorCore::MarkerAnimation) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } -public struct MarkerFingerPrint : Swift.Equatable, Swift.Hashable { - public let id: Swift.Int - public let icon: Swift.Int? - public let clickable: Swift.Int - public let draggable: Swift.Int - public let latitude: Swift.Int - public let longitude: Swift.Int - public let zIndex: Swift.Int? - public let animation: Swift.Int? - public static func == (a: MapConductorCore.MarkerFingerPrint, b: MapConductorCore.MarkerFingerPrint) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public struct MarkerFingerPrint : Swift::Equatable, Swift::Hashable { + public let id: Swift::Int + public let icon: Swift::Int? + public let clickable: Swift::Int + public let draggable: Swift::Int + public let latitude: Swift::Int + public let longitude: Swift::Int + public let zIndex: Swift::Int? + public let animation: Swift::Int? + public static func == (a: MapConductorCore::MarkerFingerPrint, b: MapConductorCore::MarkerFingerPrint) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } -public typealias OnMarkerEventHandler = (MapConductorCore.MarkerState) -> Swift.Void -final public class MarkerState : Combine.ObservableObject, Swift.Identifiable, Swift.Equatable, Swift.Hashable { - final public let id: Swift.String +public typealias OnMarkerEventHandler = (MapConductorCore::MarkerState) -> Swift::Void +final public class MarkerState : Combine::ObservableObject, Swift::Identifiable, Swift::Equatable, Swift::Hashable { + final public let id: Swift::String final public var extra: Any? - @Combine.Published<(any MapConductorCore.MarkerIconProtocol)?> @_projectedValueProperty($icon) final public var icon: (any MapConductorCore.MarkerIconProtocol)? { + final public let mutations: MapConductorCore::StateMutationSignal + @Combine::Published<(any MapConductorCore::MarkerIconProtocol)?> @_projectedValueProperty($icon) final public var icon: (any MapConductorCore::MarkerIconProtocol)? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $icon: Combine.Published<(any MapConductorCore.MarkerIconProtocol)?>.Publisher { + final public var $icon: Combine::Published<(any MapConductorCore::MarkerIconProtocol)?>.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($clickable) final public var clickable: Swift.Bool { + @Combine::Published @_projectedValueProperty($clickable) final public var clickable: Swift::Bool { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $clickable: Combine.Published.Publisher { + final public var $clickable: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($draggable) final public var draggable: Swift.Bool { + @Combine::Published @_projectedValueProperty($draggable) final public var draggable: Swift::Bool { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $draggable: Combine.Published.Publisher { + final public var $draggable: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($zIndex) final public var zIndex: Swift.Int? { + @Combine::Published @_projectedValueProperty($zIndex) final public var zIndex: Swift::Int? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $zIndex: Combine.Published.Publisher { + final public var $zIndex: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore.OnMarkerEventHandler? { + @Combine::Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore::OnMarkerEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onClick: Combine.Published.Publisher { + final public var $onClick: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onDragStart) final public var onDragStart: MapConductorCore.OnMarkerEventHandler? { + @Combine::Published @_projectedValueProperty($onDragStart) final public var onDragStart: MapConductorCore::OnMarkerEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onDragStart: Combine.Published.Publisher { + final public var $onDragStart: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onDrag) final public var onDrag: MapConductorCore.OnMarkerEventHandler? { + @Combine::Published @_projectedValueProperty($onDrag) final public var onDrag: MapConductorCore::OnMarkerEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onDrag: Combine.Published.Publisher { + final public var $onDrag: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onDragEnd) final public var onDragEnd: MapConductorCore.OnMarkerEventHandler? { + @Combine::Published @_projectedValueProperty($onDragEnd) final public var onDragEnd: MapConductorCore::OnMarkerEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onDragEnd: Combine.Published.Publisher { + final public var $onDragEnd: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onAnimateStart) final public var onAnimateStart: MapConductorCore.OnMarkerEventHandler? { + @Combine::Published @_projectedValueProperty($onAnimateStart) final public var onAnimateStart: MapConductorCore::OnMarkerEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onAnimateStart: Combine.Published.Publisher { + final public var $onAnimateStart: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onAnimateEnd) final public var onAnimateEnd: MapConductorCore.OnMarkerEventHandler? { + @Combine::Published @_projectedValueProperty($onAnimateEnd) final public var onAnimateEnd: MapConductorCore::OnMarkerEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onAnimateEnd: Combine.Published.Publisher { + final public var $onAnimateEnd: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($position) final public var position: MapConductorCore.GeoPoint { + @Combine::Published @_projectedValueProperty($position) final public var position: MapConductorCore::GeoPoint { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $position: Combine.Published.Publisher { + final public var $position: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - public init(position: MapConductorCore.GeoPoint, id: Swift.String? = nil, extra: Any? = nil, icon: (any MapConductorCore.MarkerIconProtocol)? = nil, animation: MapConductorCore.MarkerAnimation? = nil, clickable: Swift.Bool = true, draggable: Swift.Bool = false, zIndex: Swift.Int? = nil, onClick: MapConductorCore.OnMarkerEventHandler? = nil, onDragStart: MapConductorCore.OnMarkerEventHandler? = nil, onDrag: MapConductorCore.OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore.OnMarkerEventHandler? = nil) - convenience public init(position: MapConductorCore.GeoPoint, id: Swift.String? = nil, extra: Any? = nil, icon: MapConductorCore.DefaultMarkerIcon, animation: MapConductorCore.MarkerAnimation? = nil, clickable: Swift.Bool = true, draggable: Swift.Bool = false, zIndex: Swift.Int? = nil, onClick: MapConductorCore.OnMarkerEventHandler? = nil, onDragStart: MapConductorCore.OnMarkerEventHandler? = nil, onDrag: MapConductorCore.OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore.OnMarkerEventHandler? = nil) - final public func animate(_ animation: MapConductorCore.MarkerAnimation?) - final public func getAnimation() -> MapConductorCore.MarkerAnimation? - final public func copy(id: Swift.String? = nil, position: MapConductorCore.GeoPoint? = nil, extra: Any? = nil, icon: (any MapConductorCore.MarkerIconProtocol)? = nil, animation: MapConductorCore.MarkerAnimation?? = nil, clickable: Swift.Bool? = nil, draggable: Swift.Bool? = nil, zIndex: Swift.Int?? = nil, onClick: MapConductorCore.OnMarkerEventHandler? = nil, onDragStart: MapConductorCore.OnMarkerEventHandler? = nil, onDrag: MapConductorCore.OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore.OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore.OnMarkerEventHandler? = nil) -> MapConductorCore.MarkerState - public static func == (lhs: MapConductorCore.MarkerState, rhs: MapConductorCore.MarkerState) -> Swift.Bool - final public func hash(into hasher: inout Swift.Hasher) - final public func hashCode() -> Swift.Int - final public func fingerPrint() -> MapConductorCore.MarkerFingerPrint - final public func asFlow() -> Combine.AnyPublisher - public typealias ID = Swift.String - public typealias ObjectWillChangePublisher = Combine.ObservableObjectPublisher + public init(position: MapConductorCore::GeoPoint, id: Swift::String? = nil, extra: Any? = nil, icon: (any MapConductorCore::MarkerIconProtocol)? = nil, animation: MapConductorCore::MarkerAnimation? = nil, clickable: Swift::Bool = true, draggable: Swift::Bool = false, zIndex: Swift::Int? = nil, onClick: MapConductorCore::OnMarkerEventHandler? = nil, onDragStart: MapConductorCore::OnMarkerEventHandler? = nil, onDrag: MapConductorCore::OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore::OnMarkerEventHandler? = nil) + convenience public init(position: MapConductorCore::GeoPoint, id: Swift::String? = nil, extra: Any? = nil, icon: MapConductorCore::DefaultMarkerIcon, animation: MapConductorCore::MarkerAnimation? = nil, clickable: Swift::Bool = true, draggable: Swift::Bool = false, zIndex: Swift::Int? = nil, onClick: MapConductorCore::OnMarkerEventHandler? = nil, onDragStart: MapConductorCore::OnMarkerEventHandler? = nil, onDrag: MapConductorCore::OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore::OnMarkerEventHandler? = nil) + final public func animate(_ animation: MapConductorCore::MarkerAnimation?) + final public func getAnimation() -> MapConductorCore::MarkerAnimation? + final public func copy(id: Swift::String? = nil, position: MapConductorCore::GeoPoint? = nil, extra: Any? = nil, icon: (any MapConductorCore::MarkerIconProtocol)? = nil, animation: MapConductorCore::MarkerAnimation?? = nil, clickable: Swift::Bool? = nil, draggable: Swift::Bool? = nil, zIndex: Swift::Int?? = nil, onClick: MapConductorCore::OnMarkerEventHandler? = nil, onDragStart: MapConductorCore::OnMarkerEventHandler? = nil, onDrag: MapConductorCore::OnMarkerEventHandler? = nil, onDragEnd: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateStart: MapConductorCore::OnMarkerEventHandler? = nil, onAnimateEnd: MapConductorCore::OnMarkerEventHandler? = nil) -> MapConductorCore::MarkerState + public static func == (lhs: MapConductorCore::MarkerState, rhs: MapConductorCore::MarkerState) -> Swift::Bool + final public func hash(into hasher: inout Swift::Hasher) + final public func hashCode() -> Swift::Int + final public func fingerPrint() -> MapConductorCore::MarkerFingerPrint + final public func asFlow() -> Combine::AnyPublisher + public typealias ID = Swift::String + public typealias ObjectWillChangePublisher = Combine::ObservableObjectPublisher @objc deinit - final public var hashValue: Swift.Int { + final public var hashValue: Swift::Int { get } } -final public class MarkerTileRenderer : MapConductorCore.TileProvider { - final public let tileSize: Swift.Int - final public let extraIconScale: Swift.Double - public init(markerManager: MapConductorCore.MarkerManager, tileSize: Swift.Int = 256, extraIconScale: Swift.Double = 1.0, cacheSizeBytes: Swift.Int = 8 * 1024 * 1024, debugTileOverlay: Swift.Bool = false, iconScaleCallback: ((MapConductorCore.MarkerState, Swift.Int) -> Swift.Double)? = nil) +public enum MarkerTilePhaseTrace { + public static var enabled: Swift::Bool +} +final public class MarkerTileRenderer : MapConductorCore::TileProvider { + final public let tileSize: Swift::Int + final public let extraIconScale: Swift::Double + public init(markerManager: MapConductorCore::MarkerManager, tileSize: Swift::Int = 256, extraIconScale: Swift::Double = 1.0, cacheSizeBytes: Swift::Int = 8 * 1024 * 1024, debugTileOverlay: Swift::Bool = false, iconScaleCallback: ((MapConductorCore::MarkerState, Swift::Int) -> Swift::Double)? = nil, declutterPx: Swift::Int = 0) final public func invalidate() final public func clear() - final public func hitTest(screenPoint: CoreFoundation.CGPoint, markerIds: Swift.Set, zoom: Swift.Int, tolerance: CoreFoundation.CGFloat = 14, renderScaleToScreenScale: CoreFoundation.CGFloat = UIScreen.main.scale, unproject: (CoreFoundation.CGPoint) -> MapConductorCore.GeoPoint?, project: (MapConductorCore.GeoPoint) -> CoreFoundation.CGPoint?) -> MapConductorCore.MarkerState? - final public func renderTile(request: MapConductorCore.TileRequest) -> Foundation.Data? + final public func hitTest(screenPoint: CoreFoundation::CGPoint, markerIds: Swift::Set, zoom: Swift::Int, tolerance: CoreFoundation::CGFloat = 14, renderScaleToScreenScale: CoreFoundation::CGFloat = UIScreen.main.scale, unproject: (CoreFoundation::CGPoint) -> MapConductorCore::GeoPoint?, project: (MapConductorCore::GeoPoint) -> CoreFoundation::CGPoint?) -> MapConductorCore::MarkerState? + final public var onEmptyTileWhilePopulated: ((MapConductorCore::TileRequest) -> Swift::Void)? + final public func tileStillEmpty(request: MapConductorCore::TileRequest) -> Swift::Bool + final public func renderTile(request: MapConductorCore::TileRequest) -> Foundation::Data? @objc deinit } public struct MarkerTilingOptions { - public let enabled: Swift.Bool - public let debugTileOverlay: Swift.Bool - public let minMarkerCount: Swift.Int - public let cacheSize: Swift.Int - public let iconScaleCallback: ((MapConductorCore.MarkerState, Swift.Int) -> Swift.Double)? - public static let Disabled: MapConductorCore.MarkerTilingOptions - public static let Default: MapConductorCore.MarkerTilingOptions - public init(enabled: Swift.Bool = true, debugTileOverlay: Swift.Bool = false, minMarkerCount: Swift.Int = 2000, cacheSize: Swift.Int = 8 * 1024 * 1024, iconScaleCallback: ((MapConductorCore.MarkerState, Swift.Int) -> Swift.Double)? = nil) - public func shouldUseTiles(markerCount: Swift.Int) -> Swift.Bool -} -final public class StrategyMarkerController : MapConductorCore.OverlayControllerProtocol, MapConductorCore.SlottedOverlayController where ActualMarker == Strategy.ActualMarker, Strategy : MapConductorCore.MarkerRenderingStrategyProtocol, Renderer : MapConductorCore.MarkerOverlayRendererProtocol, Strategy.ActualMarker == Renderer.ActualMarker { - public typealias StateType = MapConductorCore.MarkerState - public typealias EntityType = MapConductorCore.MarkerEntity - public typealias EventType = MapConductorCore.MarkerState - final public let markerManager: MapConductorCore.MarkerManager + public let enabled: Swift::Bool + public let debugTileOverlay: Swift::Bool + public let minMarkerCount: Swift::Int + public let cacheSize: Swift::Int + public let iconScaleCallback: ((MapConductorCore::MarkerState, Swift::Int) -> Swift::Double)? + public let declutterPx: Swift::Int + public static let Disabled: MapConductorCore::MarkerTilingOptions + public static let Default: MapConductorCore::MarkerTilingOptions + public init(enabled: Swift::Bool = true, debugTileOverlay: Swift::Bool = false, minMarkerCount: Swift::Int = 2000, cacheSize: Swift::Int = 8 * 1024 * 1024, iconScaleCallback: ((MapConductorCore::MarkerState, Swift::Int) -> Swift::Double)? = nil, declutterPx: Swift::Int = 0) + public func shouldUseTiles(markerCount: Swift::Int) -> Swift::Bool +} +final public class StrategyMarkerController : MapConductorCore::OverlayControllerProtocol, MapConductorCore::SlottedOverlayController where ActualMarker == Strategy.ActualMarker, Strategy : MapConductorCore::MarkerRenderingStrategyProtocol, Renderer : MapConductorCore::MarkerOverlayRendererProtocol, Strategy.ActualMarker == Renderer.ActualMarker { + public typealias StateType = MapConductorCore::MarkerState + public typealias EntityType = MapConductorCore::MarkerEntity + public typealias EventType = MapConductorCore::MarkerState + final public let markerManager: MapConductorCore::MarkerManager final public let strategy: Strategy final public var renderer: Renderer - final public var clickListener: ((MapConductorCore.MarkerState) -> Swift.Void)? - final public var markerProjector: ((any MapConductorCore.GeoPointProtocol) -> CoreFoundation.CGPoint?)? - final public var dragStartListener: MapConductorCore.OnMarkerEventHandler? - final public var dragListener: MapConductorCore.OnMarkerEventHandler? - final public var dragEndListener: MapConductorCore.OnMarkerEventHandler? - final public var animateStartListener: MapConductorCore.OnMarkerEventHandler? - final public var animateEndListener: MapConductorCore.OnMarkerEventHandler? - final public let zIndex: Swift.Int - public init(strategy: Strategy, renderer: Renderer, clickListener: ((MapConductorCore.MarkerState) -> Swift.Void)? = nil) - final public func dispatchClick(_ state: MapConductorCore.MarkerState) - final public func dispatchDragStart(_ state: MapConductorCore.MarkerState) - final public func dispatchDrag(_ state: MapConductorCore.MarkerState) - final public func dispatchDragEnd(_ state: MapConductorCore.MarkerState) - final public func dispatchAnimateStart(_ state: MapConductorCore.MarkerState) - final public func dispatchAnimateEnd(_ state: MapConductorCore.MarkerState) - final public func add(data: [MapConductorCore.MarkerState]) async - final public func update(state: MapConductorCore.MarkerState) async + final public var clickListener: ((MapConductorCore::MarkerState) -> Swift::Void)? + final public var markerProjector: ((any MapConductorCore::GeoPointProtocol) -> CoreFoundation::CGPoint?)? + final public var dragStartListener: MapConductorCore::OnMarkerEventHandler? + final public var dragListener: MapConductorCore::OnMarkerEventHandler? + final public var dragEndListener: MapConductorCore::OnMarkerEventHandler? + final public var animateStartListener: MapConductorCore::OnMarkerEventHandler? + final public var animateEndListener: MapConductorCore::OnMarkerEventHandler? + final public let zIndex: Swift::Int + public init(strategy: Strategy, renderer: Renderer, clickListener: ((MapConductorCore::MarkerState) -> Swift::Void)? = nil) + final public func dispatchClick(_ state: MapConductorCore::MarkerState) + final public func dispatchDragStart(_ state: MapConductorCore::MarkerState) + final public func dispatchDrag(_ state: MapConductorCore::MarkerState) + final public func dispatchDragEnd(_ state: MapConductorCore::MarkerState) + final public func dispatchAnimateStart(_ state: MapConductorCore::MarkerState) + final public func dispatchAnimateEnd(_ state: MapConductorCore::MarkerState) + final public func add(data: [MapConductorCore::MarkerState]) async + final public func update(state: MapConductorCore::MarkerState) async final public func clear() async - final public func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.MarkerEntity? - final public func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + final public func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::MarkerEntity? + final public func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async final public func destroy() - final public var kind: MapConductorCore.OverlayKind { + final public var kind: MapConductorCore::OverlayKind { get } - final public func hasId(_ id: Swift.String) -> Swift.Bool - final public func resolveTap(position _: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.OverlayHit? + final public func hasId(_ id: Swift::String) -> Swift::Bool + final public func resolveTap(position _: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::OverlayHit? @objc deinit } -@_Concurrency.MainActor final public class StrategyMarkerManager : MapConductorCore.MarkerRenderingSupport where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore.MarkerOverlayRendererProtocol { - public typealias RendererFactory = (MapConductorCore.AnyMarkerRenderingStrategy) -> Renderer - @_Concurrency.MainActor final public var controller: MapConductorCore.StrategyMarkerController, Renderer>? { +@_Concurrency::MainActor final public class StrategyMarkerManager : MapConductorCore::MarkerRenderingSupport where ActualMarker == Renderer.ActualMarker, Renderer : MapConductorCore::MarkerOverlayRendererProtocol { + public typealias RendererFactory = (MapConductorCore::AnyMarkerRenderingStrategy) -> Renderer + @_Concurrency::MainActor final public var controller: MapConductorCore::StrategyMarkerController, Renderer>? { get } - @_Concurrency.MainActor final public var renderer: Renderer? { + @_Concurrency::MainActor final public var renderer: Renderer? { get } - @_Concurrency.MainActor final public var latestMarkers: [MapConductorCore.MarkerState] { + @_Concurrency::MainActor final public var latestMarkers: [MapConductorCore::MarkerState] { get } - @_Concurrency.MainActor final public var onMarkersChanged: (([MapConductorCore.MarkerState]) -> Swift.Void)? - @_Concurrency.MainActor final public var onRendererCreated: ((Renderer) -> Swift.Void)? - @_Concurrency.MainActor public init(makeRenderer: @escaping MapConductorCore.StrategyMarkerManager.RendererFactory, shouldAddMarkers: @escaping () -> Swift.Bool = { true }, currentCamera: @escaping () -> MapConductorCore.MapCameraPosition? = { nil }) + @_Concurrency::MainActor final public var onMarkersChanged: (([MapConductorCore::MarkerState]) -> Swift::Void)? + @_Concurrency::MainActor final public var onRendererCreated: ((Renderer) -> Swift::Void)? + @_Concurrency::MainActor public init(makeRenderer: @escaping MapConductorCore::StrategyMarkerManager.MapConductorCore::RendererFactory, shouldAddMarkers: @escaping () -> Swift::Bool = { true }, currentCamera: @escaping () -> MapConductorCore::MapCameraPosition? = { nil }) @discardableResult - @_Concurrency.MainActor final public func connect(strategy: Any, markers: [MapConductorCore.MarkerState]) -> Swift.Bool - @_Concurrency.MainActor final public func disconnect() - @_Concurrency.MainActor final public func beginContentPass() - @_Concurrency.MainActor final public func endContentPass() - @_Concurrency.MainActor final public func onCameraChanged(_ position: MapConductorCore.MapCameraPosition) async - @_Concurrency.MainActor final public func flush() - @_Concurrency.MainActor final public func clear() - @_Concurrency.MainActor final public func syncMarkers(_ markers: [MapConductorCore.MarkerState]) + @_Concurrency::MainActor final public func connect(strategy: Any, markers: [MapConductorCore::MarkerState]) -> Swift::Bool + @_Concurrency::MainActor final public func disconnect() + @_Concurrency::MainActor final public func beginContentPass() + @_Concurrency::MainActor final public func endContentPass() + @_Concurrency::MainActor final public func onCameraChanged(_ position: MapConductorCore::MapCameraPosition) async + @_Concurrency::MainActor final public func flush() + @_Concurrency::MainActor final public func clear() + @_Concurrency::MainActor final public func syncMarkers(_ markers: [MapConductorCore::MarkerState]) @objc deinit } -@_Concurrency.MainActor final public class DeferredUntilReady { - @_Concurrency.MainActor final public var isReady: Swift.Bool { +@_Concurrency::MainActor final public class DeferredUntilReady { + @_Concurrency::MainActor final public var isReady: Swift::Bool { get } - @_Concurrency.MainActor final public var latest: Input? { + @_Concurrency::MainActor final public var latest: Input? { get } - @_Concurrency.MainActor public init(apply: @escaping (Input) -> Swift.Void) - @_Concurrency.MainActor final public func submit(_ input: Input) - @_Concurrency.MainActor final public func markReady() - @_Concurrency.MainActor final public func reset() + @_Concurrency::MainActor public init(apply: @escaping (Input) -> Swift::Void) + @_Concurrency::MainActor final public func submit(_ input: Input) + @_Concurrency::MainActor final public func markReady() + @_Concurrency::MainActor final public func reset() @objc deinit } -@_Concurrency.MainActor final public class MapOverlayScope { - @_Concurrency.MainActor final public let markerCollector: MapConductorCore.OverlayCollector - @_Concurrency.MainActor final public let circleCollector: MapConductorCore.OverlayCollector - @_Concurrency.MainActor final public let polylineCollector: MapConductorCore.OverlayCollector - @_Concurrency.MainActor final public let polygonCollector: MapConductorCore.OverlayCollector - @_Concurrency.MainActor final public let groundImageCollector: MapConductorCore.OverlayCollector - @_Concurrency.MainActor final public let rasterLayerCollector: MapConductorCore.OverlayCollector - @_Concurrency.MainActor public init() - @_Concurrency.MainActor final public func clear() +@_Concurrency::MainActor final public class MapOverlayScope { + @_Concurrency::MainActor final public let markerCollector: MapConductorCore::OverlayCollector + @_Concurrency::MainActor final public let circleCollector: MapConductorCore::OverlayCollector + @_Concurrency::MainActor final public let polylineCollector: MapConductorCore::OverlayCollector + @_Concurrency::MainActor final public let polygonCollector: MapConductorCore::OverlayCollector + @_Concurrency::MainActor final public let groundImageCollector: MapConductorCore::OverlayCollector + @_Concurrency::MainActor final public let rasterLayerCollector: MapConductorCore::OverlayCollector + @_Concurrency::MainActor public init() + @_Concurrency::MainActor final public func clear() @objc deinit } public protocol OverlayCollectableState : AnyObject { - var id: Swift.String { get } - func overlayChangePublisher() -> Combine.AnyPublisher -} -extension MapConductorCore.MarkerState : MapConductorCore.OverlayCollectableState { - final public func overlayChangePublisher() -> Combine.AnyPublisher -} -extension MapConductorCore.CircleState : MapConductorCore.OverlayCollectableState { - final public func overlayChangePublisher() -> Combine.AnyPublisher -} -extension MapConductorCore.PolylineState : MapConductorCore.OverlayCollectableState { - final public func overlayChangePublisher() -> Combine.AnyPublisher -} -extension MapConductorCore.PolygonState : MapConductorCore.OverlayCollectableState { - final public func overlayChangePublisher() -> Combine.AnyPublisher -} -extension MapConductorCore.GroundImageState : MapConductorCore.OverlayCollectableState { - final public func overlayChangePublisher() -> Combine.AnyPublisher -} -extension MapConductorCore.RasterLayerState : MapConductorCore.OverlayCollectableState { - final public func overlayChangePublisher() -> Combine.AnyPublisher -} -@_Concurrency.MainActor final public class OverlayCollector where S : MapConductorCore.OverlayCollectableState { - @_Concurrency.MainActor public init(shouldApply: @escaping () -> Swift.Bool = { true }) - @_Concurrency.MainActor final public func setShouldApply(_ gate: @escaping () -> Swift.Bool) - @_Concurrency.MainActor final public func onMembershipChange(_ handler: @escaping ([S]) -> Swift.Void) - @_Concurrency.MainActor final public func onStateChange(_ handler: @escaping (S) -> Swift.Void) - @_Concurrency.MainActor final public func sync(_ states: [S]) - @_Concurrency.MainActor final public func flush() - @_Concurrency.MainActor final public func values() -> [S] - @_Concurrency.MainActor final public func get(_ id: Swift.String) -> S? - @_Concurrency.MainActor final public func clear() - @objc deinit -} -@_Concurrency.MainActor public func bindOverlayCollector(_ collector: MapConductorCore.OverlayCollector, to controller: C) where S : MapConductorCore.OverlayCollectableState, S == C.StateType, C : MapConductorCore.OverlayControllerProtocol -public func bridgeHolesIntoSingleRing(outer: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]], separation: Swift.Double = 0.0) -> [any MapConductorCore.GeoPointProtocol] -public func bridgeHolesIntoSingleRingWrapAware(outer: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]], separation: Swift.Double = 0.0) -> [any MapConductorCore.GeoPointProtocol] -open class PolygonController : MapConductorCore.OverlayControllerProtocol, MapConductorCore.SlottedOverlayController where ActualPolygon == Renderer.ActualPolygon, Renderer : MapConductorCore.PolygonOverlayRendererProtocol { - public typealias StateType = MapConductorCore.PolygonState - public typealias EntityType = MapConductorCore.PolygonEntity - public typealias EventType = MapConductorCore.PolygonEvent - final public let polygonManager: MapConductorCore.PolygonManager + var id: Swift::String { get } + var mutations: MapConductorCore::StateMutationSignal { get } +} +extension MapConductorCore::MarkerState : MapConductorCore::OverlayCollectableState { +} +extension MapConductorCore::CircleState : MapConductorCore::OverlayCollectableState { +} +extension MapConductorCore::PolylineState : MapConductorCore::OverlayCollectableState { +} +extension MapConductorCore::PolygonState : MapConductorCore::OverlayCollectableState { +} +extension MapConductorCore::GroundImageState : MapConductorCore::OverlayCollectableState { +} +extension MapConductorCore::RasterLayerState : MapConductorCore::OverlayCollectableState { +} +@_Concurrency::MainActor final public class OverlayCollector where S : MapConductorCore::OverlayCollectableState { + @_Concurrency::MainActor public init(shouldApply: @escaping () -> Swift::Bool = { true }) + @_Concurrency::MainActor final public func setShouldApply(_ gate: @escaping () -> Swift::Bool) + @_Concurrency::MainActor final public func onMembershipChange(_ handler: @escaping ([S]) -> Swift::Void) + @_Concurrency::MainActor final public func onStateChange(_ handler: @escaping (S) -> Swift::Void) + @_Concurrency::MainActor final public func sync(_ states: [S]) + @_Concurrency::MainActor final public func flush() + @_Concurrency::MainActor final public func values() -> [S] + @_Concurrency::MainActor final public func get(_ id: Swift::String) -> S? + @_Concurrency::MainActor final public func clear() + @objc deinit +} +@_Concurrency::MainActor public func bindOverlayCollector(_ collector: MapConductorCore::OverlayCollector, to controller: C) where S : MapConductorCore::OverlayCollectableState, S == C.StateType, C : MapConductorCore::OverlayControllerProtocol +final public class StateMutationSignal { + public init() + final public func listen(_ listener: (() -> Swift::Void)?) + final public func notifyMutated() + @objc deinit +} +public func bridgeHolesIntoSingleRing(outer: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]], separation: Swift::Double = 0.0) -> [any MapConductorCore::GeoPointProtocol] +public func bridgeHolesIntoSingleRingWrapAware(outer: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]], separation: Swift::Double = 0.0) -> [any MapConductorCore::GeoPointProtocol] +open class PolygonController : MapConductorCore::OverlayControllerProtocol, MapConductorCore::SlottedOverlayController where ActualPolygon == Renderer.ActualPolygon, Renderer : MapConductorCore::PolygonOverlayRendererProtocol { + public typealias StateType = MapConductorCore::PolygonState + public typealias EntityType = MapConductorCore::PolygonEntity + public typealias EventType = MapConductorCore::PolygonEvent + final public let polygonManager: MapConductorCore::PolygonManager open var renderer: Renderer - final public let zIndex: Swift.Int - public var clickListener: ((MapConductorCore.PolygonEvent) -> Swift.Void)? - public init(polygonManager: MapConductorCore.PolygonManager, renderer: Renderer, clickListener: ((MapConductorCore.PolygonEvent) -> Swift.Void)? = nil) - public func dispatchClick(event: MapConductorCore.PolygonEvent) - open func add(data: [MapConductorCore.PolygonState]) async - open func update(state: MapConductorCore.PolygonState) async + final public let zIndex: Swift::Int + public var clickListener: ((MapConductorCore::PolygonEvent) -> Swift::Void)? + public init(polygonManager: MapConductorCore::PolygonManager, renderer: Renderer, clickListener: ((MapConductorCore::PolygonEvent) -> Swift::Void)? = nil) + public func dispatchClick(event: MapConductorCore::PolygonEvent) + open func add(data: [MapConductorCore::PolygonState]) async + open func update(state: MapConductorCore::PolygonState) async open func clear() async - open func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.PolygonEntity? - open func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + open func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::PolygonEntity? + open func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async open func destroy() - public var kind: MapConductorCore.OverlayKind { + public var kind: MapConductorCore::OverlayKind { get } - public func hasId(_ id: Swift.String) -> Swift.Bool - public func resolveTap(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.OverlayHit? + public func hasId(_ id: Swift::String) -> Swift::Bool + public func resolveTap(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::OverlayHit? @objc deinit } public protocol PolygonEntityProtocol { associatedtype ActualPolygon var polygon: Self.ActualPolygon? { get set } - var state: MapConductorCore.PolygonState { get } - var fingerPrint: MapConductorCore.PolygonFingerPrint { get } + var state: MapConductorCore::PolygonState { get } + var fingerPrint: MapConductorCore::PolygonFingerPrint { get } } -final public class PolygonEntity : MapConductorCore.PolygonEntityProtocol { +final public class PolygonEntity : MapConductorCore::PolygonEntityProtocol { final public var polygon: ActualPolygon? - final public let state: MapConductorCore.PolygonState - final public let fingerPrint: MapConductorCore.PolygonFingerPrint - public init(polygon: ActualPolygon?, state: MapConductorCore.PolygonState) + final public let state: MapConductorCore::PolygonState + final public let fingerPrint: MapConductorCore::PolygonFingerPrint + public init(polygon: ActualPolygon?, state: MapConductorCore::PolygonState) @objc deinit } -public func partitionPolygonByHoles(outer: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]]) -> [(outer: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]])] -public func splitPolygonWithHolesIntoSimpleRings(outer: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]]) -> [[any MapConductorCore.GeoPointProtocol]] +public func partitionPolygonByHoles(outer: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]]) -> [(outer: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]])] +public func splitPolygonWithHolesIntoSimpleRings(outer: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]]) -> [[any MapConductorCore::GeoPointProtocol]] public protocol PolygonManagerProtocol { associatedtype ActualPolygon - func registerEntity(_ entity: MapConductorCore.PolygonEntity) - func removeEntity(_ id: Swift.String) -> MapConductorCore.PolygonEntity? - func getEntity(_ id: Swift.String) -> MapConductorCore.PolygonEntity? - func hasEntity(_ id: Swift.String) -> Swift.Bool - func allEntities() -> [MapConductorCore.PolygonEntity] + func registerEntity(_ entity: MapConductorCore::PolygonEntity) + func removeEntity(_ id: Swift::String) -> MapConductorCore::PolygonEntity? + func getEntity(_ id: Swift::String) -> MapConductorCore::PolygonEntity? + func hasEntity(_ id: Swift::String) -> Swift::Bool + func allEntities() -> [MapConductorCore::PolygonEntity] func clear() - func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.PolygonEntity? + func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::PolygonEntity? } -final public class PolygonManager : MapConductorCore.PolygonManagerProtocol { +final public class PolygonManager : MapConductorCore::PolygonManagerProtocol { public init() - final public func registerEntity(_ entity: MapConductorCore.PolygonEntity) - final public func removeEntity(_ id: Swift.String) -> MapConductorCore.PolygonEntity? - final public func getEntity(_ id: Swift.String) -> MapConductorCore.PolygonEntity? - final public func hasEntity(_ id: Swift.String) -> Swift.Bool - final public func allEntities() -> [MapConductorCore.PolygonEntity] + final public func registerEntity(_ entity: MapConductorCore::PolygonEntity) + final public func removeEntity(_ id: Swift::String) -> MapConductorCore::PolygonEntity? + final public func getEntity(_ id: Swift::String) -> MapConductorCore::PolygonEntity? + final public func hasEntity(_ id: Swift::String) -> Swift::Bool + final public func allEntities() -> [MapConductorCore::PolygonEntity] final public func clear() final public func destroy() - final public var isDestroyed: Swift.Bool { + final public var isDestroyed: Swift::Bool { get } - final public func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.PolygonEntity? + final public func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::PolygonEntity? @objc deinit } public protocol PolygonOverlayRendererProtocol { associatedtype ActualPolygon - func onAdd(data: [MapConductorCore.PolygonOverlayAddParams]) async -> [Self.ActualPolygon?] - func onChange(data: [MapConductorCore.PolygonOverlayChangeParams]) async -> [Self.ActualPolygon?] - func onRemove(data: [MapConductorCore.PolygonEntity]) async + func onAdd(data: [MapConductorCore::PolygonOverlayAddParams]) async -> [Self.ActualPolygon?] + func onChange(data: [MapConductorCore::PolygonOverlayChangeParams]) async -> [Self.ActualPolygon?] + func onRemove(data: [MapConductorCore::PolygonEntity]) async func onPostProcess() async } -open class AbstractPolygonOverlayRenderer : MapConductorCore.PolygonOverlayRendererProtocol { +open class AbstractPolygonOverlayRenderer : MapConductorCore::PolygonOverlayRendererProtocol { public init() open func onPostProcess() async - open func createPolygon(state: MapConductorCore.PolygonState) async -> ActualPolygon? - open func updatePolygonProperties(polygon: ActualPolygon, current: MapConductorCore.PolygonEntity, prev: MapConductorCore.PolygonEntity) async -> ActualPolygon? - open func removePolygon(entity: MapConductorCore.PolygonEntity) async - public func onAdd(data: [MapConductorCore.PolygonOverlayAddParams]) async -> [ActualPolygon?] - public func onChange(data: [MapConductorCore.PolygonOverlayChangeParams]) async -> [ActualPolygon?] - public func onRemove(data: [MapConductorCore.PolygonEntity]) async + open func createPolygon(state: MapConductorCore::PolygonState) async -> ActualPolygon? + open func updatePolygonProperties(polygon: ActualPolygon, current: MapConductorCore::PolygonEntity, prev: MapConductorCore::PolygonEntity) async -> ActualPolygon? + open func removePolygon(entity: MapConductorCore::PolygonEntity) async + public func onAdd(data: [MapConductorCore::PolygonOverlayAddParams]) async -> [ActualPolygon?] + public func onChange(data: [MapConductorCore::PolygonOverlayChangeParams]) async -> [ActualPolygon?] + public func onRemove(data: [MapConductorCore::PolygonEntity]) async @objc deinit } public struct PolygonOverlayAddParams { - public let state: MapConductorCore.PolygonState - public init(state: MapConductorCore.PolygonState) + public let state: MapConductorCore::PolygonState + public init(state: MapConductorCore::PolygonState) } public struct PolygonOverlayChangeParams { - public let current: MapConductorCore.PolygonEntity - public let prev: MapConductorCore.PolygonEntity - public init(current: MapConductorCore.PolygonEntity, prev: MapConductorCore.PolygonEntity) -} -public struct PolygonFingerPrint : Swift.Equatable, Swift.Hashable { - public let id: Swift.Int - public let strokeColor: Swift.Int - public let strokeWidth: Swift.Int - public let fillColor: Swift.Int - public let geodesic: Swift.Int - public let zIndex: Swift.Int - public let points: Swift.Int - public let holes: Swift.Int - public let extra: Swift.Int - public static func == (a: MapConductorCore.PolygonFingerPrint, b: MapConductorCore.PolygonFingerPrint) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { + public let current: MapConductorCore::PolygonEntity + public let prev: MapConductorCore::PolygonEntity + public init(current: MapConductorCore::PolygonEntity, prev: MapConductorCore::PolygonEntity) +} +public struct PolygonFingerPrint : Swift::Equatable, Swift::Hashable { + public let id: Swift::Int + public let strokeColor: Swift::Int + public let strokeWidth: Swift::Int + public let fillColor: Swift::Int + public let geodesic: Swift::Int + public let zIndex: Swift::Int + public let points: Swift::Int + public let holes: Swift::Int + public let extra: Swift::Int + public static func == (a: MapConductorCore::PolygonFingerPrint, b: MapConductorCore::PolygonFingerPrint) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public struct PolygonEvent { - public let state: MapConductorCore.PolygonState - public let clicked: any MapConductorCore.GeoPointProtocol - public init(state: MapConductorCore.PolygonState, clicked: any MapConductorCore.GeoPointProtocol) + public let state: MapConductorCore::PolygonState + public let clicked: any MapConductorCore::GeoPointProtocol + public init(state: MapConductorCore::PolygonState, clicked: any MapConductorCore::GeoPointProtocol) } -public typealias OnPolygonEventHandler = (MapConductorCore.PolygonEvent) -> Swift.Void -final public class PolygonState : Combine.ObservableObject, Swift.Identifiable, Swift.Equatable, Swift.Hashable { - final public let id: Swift.String - @Combine.Published @_projectedValueProperty($strokeColor) final public var strokeColor: UIKit.UIColor { +public typealias OnPolygonEventHandler = (MapConductorCore::PolygonEvent) -> Swift::Void +final public class PolygonState : Combine::ObservableObject, Swift::Identifiable, Swift::Equatable, Swift::Hashable { + final public let id: Swift::String + final public let mutations: MapConductorCore::StateMutationSignal + @Combine::Published @_projectedValueProperty($strokeColor) final public var strokeColor: UIKit::UIColor { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $strokeColor: Combine.Published.Publisher { + final public var $strokeColor: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($strokeWidth) final public var strokeWidth: Swift.Double { + @Combine::Published @_projectedValueProperty($strokeWidth) final public var strokeWidth: Swift::Double { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $strokeWidth: Combine.Published.Publisher { + final public var $strokeWidth: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($fillColor) final public var fillColor: UIKit.UIColor { + @Combine::Published @_projectedValueProperty($fillColor) final public var fillColor: UIKit::UIColor { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $fillColor: Combine.Published.Publisher { + final public var $fillColor: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($geodesic) final public var geodesic: Swift.Bool { + @Combine::Published @_projectedValueProperty($geodesic) final public var geodesic: Swift::Bool { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $geodesic: Combine.Published.Publisher { + final public var $geodesic: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($zIndex) final public var zIndex: Swift.Int { + @Combine::Published @_projectedValueProperty($zIndex) final public var zIndex: Swift::Int { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $zIndex: Combine.Published.Publisher { + final public var $zIndex: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published<[any MapConductorCore.GeoPointProtocol]> @_projectedValueProperty($points) final public var points: [any MapConductorCore.GeoPointProtocol] { + @Combine::Published<[any MapConductorCore::GeoPointProtocol]> @_projectedValueProperty($points) final public var points: [any MapConductorCore::GeoPointProtocol] { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $points: Combine.Published<[any MapConductorCore.GeoPointProtocol]>.Publisher { + final public var $points: Combine::Published<[any MapConductorCore::GeoPointProtocol]>.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published<[[any MapConductorCore.GeoPointProtocol]]> @_projectedValueProperty($holes) final public var holes: [[any MapConductorCore.GeoPointProtocol]] { + @Combine::Published<[[any MapConductorCore::GeoPointProtocol]]> @_projectedValueProperty($holes) final public var holes: [[any MapConductorCore::GeoPointProtocol]] { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $holes: Combine.Published<[[any MapConductorCore.GeoPointProtocol]]>.Publisher { + final public var $holes: Combine::Published<[[any MapConductorCore::GeoPointProtocol]]>.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($extra) final public var extra: Any? { + @Combine::Published @_projectedValueProperty($extra) final public var extra: Any? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $extra: Combine.Published.Publisher { + final public var $extra: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore.OnPolygonEventHandler? { + @Combine::Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore::OnPolygonEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onClick: Combine.Published.Publisher { + final public var $onClick: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - public init(points: [any MapConductorCore.GeoPointProtocol], holes: [[any MapConductorCore.GeoPointProtocol]] = [], id: Swift.String? = nil, strokeColor: UIKit.UIColor = .black, strokeWidth: Swift.Double = 2.0, fillColor: UIKit.UIColor = .clear, geodesic: Swift.Bool = false, zIndex: Swift.Int = 0, extra: Any? = nil, onClick: MapConductorCore.OnPolygonEventHandler? = nil) - final public func copy(points: [any MapConductorCore.GeoPointProtocol]? = nil, holes: [[any MapConductorCore.GeoPointProtocol]]? = nil, id: Swift.String? = nil, strokeColor: UIKit.UIColor? = nil, strokeWidth: Swift.Double? = nil, fillColor: UIKit.UIColor? = nil, geodesic: Swift.Bool? = nil, zIndex: Swift.Int? = nil, extra: Any? = nil, onClick: MapConductorCore.OnPolygonEventHandler? = nil) -> MapConductorCore.PolygonState - final public func fingerPrint() -> MapConductorCore.PolygonFingerPrint - final public func asFlow() -> Combine.AnyPublisher - public static func == (lhs: MapConductorCore.PolygonState, rhs: MapConductorCore.PolygonState) -> Swift.Bool - final public func hash(into hasher: inout Swift.Hasher) - final public func hashCode() -> Swift.Int - public typealias ID = Swift.String - public typealias ObjectWillChangePublisher = Combine.ObservableObjectPublisher + public init(points: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]] = [], id: Swift::String? = nil, strokeColor: UIKit::UIColor = .black, strokeWidth: Swift::Double = 2.0, fillColor: UIKit::UIColor = .clear, geodesic: Swift::Bool = false, zIndex: Swift::Int = 0, extra: Any? = nil, onClick: MapConductorCore::OnPolygonEventHandler? = nil) + final public func copy(points: [any MapConductorCore::GeoPointProtocol]? = nil, holes: [[any MapConductorCore::GeoPointProtocol]]? = nil, id: Swift::String? = nil, strokeColor: UIKit::UIColor? = nil, strokeWidth: Swift::Double? = nil, fillColor: UIKit::UIColor? = nil, geodesic: Swift::Bool? = nil, zIndex: Swift::Int? = nil, extra: Any? = nil, onClick: MapConductorCore::OnPolygonEventHandler? = nil) -> MapConductorCore::PolygonState + final public func fingerPrint() -> MapConductorCore::PolygonFingerPrint + final public func asFlow() -> Combine::AnyPublisher + public static func == (lhs: MapConductorCore::PolygonState, rhs: MapConductorCore::PolygonState) -> Swift::Bool + final public func hash(into hasher: inout Swift::Hasher) + final public func hashCode() -> Swift::Int + public typealias ID = Swift::String + public typealias ObjectWillChangePublisher = Combine::ObservableObjectPublisher @objc deinit - final public var hashValue: Swift.Int { + final public var hashValue: Swift::Int { get } } public protocol PolygonSyncHandler : AnyObject { - func bindPolygonSync(_ polygonSync: @escaping @_Concurrency.MainActor ([MapConductorCore.PolygonState]) async -> Swift.Void) + func bindPolygonSync(_ polygonSync: @escaping @_Concurrency::MainActor ([MapConductorCore::PolygonState]) async -> Swift::Void) } -extension MapConductorCore.PolygonState { - final public func unionHoles() -> MapConductorCore.PolygonState - final public func unionHolesInBackground() async -> MapConductorCore.PolygonState +extension MapConductorCore::PolygonState { + final public func unionHoles() -> MapConductorCore::PolygonState + final public func unionHolesInBackground() async -> MapConductorCore::PolygonState @discardableResult - final public func unionHolesInPlace() -> MapConductorCore.PolygonState -} -public func polygonSignedArea(_ ring: [any MapConductorCore.GeoPointProtocol]) -> Swift.Double -public func ensureCounterClockwise(_ ring: [any MapConductorCore.GeoPointProtocol]) -> [any MapConductorCore.GeoPointProtocol] -public func ensureClockwiseRing(_ ring: [any MapConductorCore.GeoPointProtocol]) -> [any MapConductorCore.GeoPointProtocol] -open class PolylineController : MapConductorCore.OverlayControllerProtocol, MapConductorCore.SlottedOverlayController where ActualPolyline == Renderer.ActualPolyline, Renderer : MapConductorCore.PolylineOverlayRendererProtocol { - public typealias StateType = MapConductorCore.PolylineState - public typealias EntityType = MapConductorCore.PolylineEntity - public typealias EventType = MapConductorCore.PolylineEvent - final public let polylineManager: MapConductorCore.PolylineManager + final public func unionHolesInPlace() -> MapConductorCore::PolygonState +} +public func polygonSignedArea(_ ring: [any MapConductorCore::GeoPointProtocol]) -> Swift::Double +public func ensureCounterClockwise(_ ring: [any MapConductorCore::GeoPointProtocol]) -> [any MapConductorCore::GeoPointProtocol] +public func ensureClockwiseRing(_ ring: [any MapConductorCore::GeoPointProtocol]) -> [any MapConductorCore::GeoPointProtocol] +open class PolylineController : MapConductorCore::OverlayControllerProtocol, MapConductorCore::SlottedOverlayController where ActualPolyline == Renderer.ActualPolyline, Renderer : MapConductorCore::PolylineOverlayRendererProtocol { + public typealias StateType = MapConductorCore::PolylineState + public typealias EntityType = MapConductorCore::PolylineEntity + public typealias EventType = MapConductorCore::PolylineEvent + final public let polylineManager: MapConductorCore::PolylineManager open var renderer: Renderer - final public let zIndex: Swift.Int - public var clickListener: ((MapConductorCore.PolylineEvent) -> Swift.Void)? - public init(polylineManager: MapConductorCore.PolylineManager, renderer: Renderer, clickListener: ((MapConductorCore.PolylineEvent) -> Swift.Void)? = nil) - public func dispatchClick(event: MapConductorCore.PolylineEvent) - open func add(data: [MapConductorCore.PolylineState]) async - open func update(state: MapConductorCore.PolylineState) async + final public let zIndex: Swift::Int + public var clickListener: ((MapConductorCore::PolylineEvent) -> Swift::Void)? + public init(polylineManager: MapConductorCore::PolylineManager, renderer: Renderer, clickListener: ((MapConductorCore::PolylineEvent) -> Swift::Void)? = nil) + public func dispatchClick(event: MapConductorCore::PolylineEvent) + open func add(data: [MapConductorCore::PolylineState]) async + open func update(state: MapConductorCore::PolylineState) async open func clear() async - open func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.PolylineEntity? - public func findWithClosestPoint(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.PolylineHitResult? - public func setCurrentCameraPosition(_ cameraPosition: MapConductorCore.MapCameraPosition?) - open func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + open func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::PolylineEntity? + public func findWithClosestPoint(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::PolylineHitResult? + public func setCurrentCameraPosition(_ cameraPosition: MapConductorCore::MapCameraPosition?) + open func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async open func destroy() - public var kind: MapConductorCore.OverlayKind { + public var kind: MapConductorCore::OverlayKind { get } - public func hasId(_ id: Swift.String) -> Swift.Bool - public func resolveTap(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.OverlayHit? + public func hasId(_ id: Swift::String) -> Swift::Bool + public func resolveTap(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::OverlayHit? @objc deinit } public protocol PolylineEntityProtocol { associatedtype ActualPolyline var polyline: Self.ActualPolyline? { get set } - var state: MapConductorCore.PolylineState { get } - var fingerPrint: MapConductorCore.PolylineFingerPrint { get } + var state: MapConductorCore::PolylineState { get } + var fingerPrint: MapConductorCore::PolylineFingerPrint { get } } -final public class PolylineEntity : MapConductorCore.PolylineEntityProtocol { +final public class PolylineEntity : MapConductorCore::PolylineEntityProtocol { final public var polyline: ActualPolyline? - final public let state: MapConductorCore.PolylineState - final public let fingerPrint: MapConductorCore.PolylineFingerPrint - public init(polyline: ActualPolyline?, state: MapConductorCore.PolylineState) + final public let state: MapConductorCore::PolylineState + final public let fingerPrint: MapConductorCore::PolylineFingerPrint + public init(polyline: ActualPolyline?, state: MapConductorCore::PolylineState) @objc deinit } public struct PolylineHitResult { - public let entity: MapConductorCore.PolylineEntity - public let closestPoint: any MapConductorCore.GeoPointProtocol + public let entity: MapConductorCore::PolylineEntity + public let closestPoint: any MapConductorCore::GeoPointProtocol } public protocol PolylineManagerProtocol { associatedtype ActualPolyline - func registerEntity(_ entity: MapConductorCore.PolylineEntity) - func removeEntity(_ id: Swift.String) -> MapConductorCore.PolylineEntity? - func getEntity(_ id: Swift.String) -> MapConductorCore.PolylineEntity? - func hasEntity(_ id: Swift.String) -> Swift.Bool - func allEntities() -> [MapConductorCore.PolylineEntity] + func registerEntity(_ entity: MapConductorCore::PolylineEntity) + func removeEntity(_ id: Swift::String) -> MapConductorCore::PolylineEntity? + func getEntity(_ id: Swift::String) -> MapConductorCore::PolylineEntity? + func hasEntity(_ id: Swift::String) -> Swift::Bool + func allEntities() -> [MapConductorCore::PolylineEntity] func clear() - func find(position: any MapConductorCore.GeoPointProtocol, cameraPosition: MapConductorCore.MapCameraPosition?) -> MapConductorCore.PolylineHitResult? + func find(position: any MapConductorCore::GeoPointProtocol, cameraPosition: MapConductorCore::MapCameraPosition?) -> MapConductorCore::PolylineHitResult? } -final public class PolylineManager : MapConductorCore.PolylineManagerProtocol { +final public class PolylineManager : MapConductorCore::PolylineManagerProtocol { public init() - final public func registerEntity(_ entity: MapConductorCore.PolylineEntity) - final public func removeEntity(_ id: Swift.String) -> MapConductorCore.PolylineEntity? - final public func getEntity(_ id: Swift.String) -> MapConductorCore.PolylineEntity? - final public func hasEntity(_ id: Swift.String) -> Swift.Bool - final public func allEntities() -> [MapConductorCore.PolylineEntity] + final public func registerEntity(_ entity: MapConductorCore::PolylineEntity) + final public func removeEntity(_ id: Swift::String) -> MapConductorCore::PolylineEntity? + final public func getEntity(_ id: Swift::String) -> MapConductorCore::PolylineEntity? + final public func hasEntity(_ id: Swift::String) -> Swift::Bool + final public func allEntities() -> [MapConductorCore::PolylineEntity] final public func clear() final public func destroy() - final public var isDestroyed: Swift.Bool { + final public var isDestroyed: Swift::Bool { get } - final public func find(position: any MapConductorCore.GeoPointProtocol, cameraPosition: MapConductorCore.MapCameraPosition?) -> MapConductorCore.PolylineHitResult? + final public func find(position: any MapConductorCore::GeoPointProtocol, cameraPosition: MapConductorCore::MapCameraPosition?) -> MapConductorCore::PolylineHitResult? @objc deinit } public protocol PolylineOverlayRendererProtocol { associatedtype ActualPolyline - func onAdd(data: [MapConductorCore.PolylineOverlayAddParams]) async -> [Self.ActualPolyline?] - func onChange(data: [MapConductorCore.PolylineOverlayChangeParams]) async -> [Self.ActualPolyline?] - func onRemove(data: [MapConductorCore.PolylineEntity]) async + func onAdd(data: [MapConductorCore::PolylineOverlayAddParams]) async -> [Self.ActualPolyline?] + func onChange(data: [MapConductorCore::PolylineOverlayChangeParams]) async -> [Self.ActualPolyline?] + func onRemove(data: [MapConductorCore::PolylineEntity]) async func onPostProcess() async } -open class AbstractPolylineOverlayRenderer : MapConductorCore.PolylineOverlayRendererProtocol { +open class AbstractPolylineOverlayRenderer : MapConductorCore::PolylineOverlayRendererProtocol { public init() open func onPostProcess() async - open func createPolyline(state: MapConductorCore.PolylineState) async -> ActualPolyline? - open func updatePolylineProperties(polyline: ActualPolyline, current: MapConductorCore.PolylineEntity, prev: MapConductorCore.PolylineEntity) async -> ActualPolyline? - open func removePolyline(entity: MapConductorCore.PolylineEntity) async - public func onAdd(data: [MapConductorCore.PolylineOverlayAddParams]) async -> [ActualPolyline?] - public func onChange(data: [MapConductorCore.PolylineOverlayChangeParams]) async -> [ActualPolyline?] - public func onRemove(data: [MapConductorCore.PolylineEntity]) async + open func createPolyline(state: MapConductorCore::PolylineState) async -> ActualPolyline? + open func updatePolylineProperties(polyline: ActualPolyline, current: MapConductorCore::PolylineEntity, prev: MapConductorCore::PolylineEntity) async -> ActualPolyline? + open func removePolyline(entity: MapConductorCore::PolylineEntity) async + public func onAdd(data: [MapConductorCore::PolylineOverlayAddParams]) async -> [ActualPolyline?] + public func onChange(data: [MapConductorCore::PolylineOverlayChangeParams]) async -> [ActualPolyline?] + public func onRemove(data: [MapConductorCore::PolylineEntity]) async @objc deinit } public struct PolylineOverlayAddParams { - public let state: MapConductorCore.PolylineState - public init(state: MapConductorCore.PolylineState) + public let state: MapConductorCore::PolylineState + public init(state: MapConductorCore::PolylineState) } public struct PolylineOverlayChangeParams { - public let current: MapConductorCore.PolylineEntity - public let prev: MapConductorCore.PolylineEntity - public init(current: MapConductorCore.PolylineEntity, prev: MapConductorCore.PolylineEntity) + public let current: MapConductorCore::PolylineEntity + public let prev: MapConductorCore::PolylineEntity + public init(current: MapConductorCore::PolylineEntity, prev: MapConductorCore::PolylineEntity) } -public struct PolylineFingerPrint : Swift.Equatable, Swift.Hashable { - public let id: Swift.Int - public let strokeColor: Swift.Int - public let strokeWidth: Swift.Int - public let geodesic: Swift.Int - public let points: Swift.Int - public let zIndex: Swift.Int - public let extra: Swift.Int - public static func == (a: MapConductorCore.PolylineFingerPrint, b: MapConductorCore.PolylineFingerPrint) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public struct PolylineFingerPrint : Swift::Equatable, Swift::Hashable { + public let id: Swift::Int + public let strokeColor: Swift::Int + public let strokeWidth: Swift::Int + public let geodesic: Swift::Int + public let points: Swift::Int + public let zIndex: Swift::Int + public let extra: Swift::Int + public static func == (a: MapConductorCore::PolylineFingerPrint, b: MapConductorCore::PolylineFingerPrint) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public struct PolylineEvent { - public let state: MapConductorCore.PolylineState - public let clicked: any MapConductorCore.GeoPointProtocol - public init(state: MapConductorCore.PolylineState, clicked: any MapConductorCore.GeoPointProtocol) + public let state: MapConductorCore::PolylineState + public let clicked: any MapConductorCore::GeoPointProtocol + public init(state: MapConductorCore::PolylineState, clicked: any MapConductorCore::GeoPointProtocol) } -public typealias OnPolylineEventHandler = (MapConductorCore.PolylineEvent) -> Swift.Void -final public class PolylineState : Combine.ObservableObject, Swift.Identifiable, Swift.Equatable, Swift.Hashable { - final public let id: Swift.String - @Combine.Published @_projectedValueProperty($strokeColor) final public var strokeColor: UIKit.UIColor { +public typealias OnPolylineEventHandler = (MapConductorCore::PolylineEvent) -> Swift::Void +final public class PolylineState : Combine::ObservableObject, Swift::Identifiable, Swift::Equatable, Swift::Hashable { + final public let id: Swift::String + final public let mutations: MapConductorCore::StateMutationSignal + @Combine::Published @_projectedValueProperty($strokeColor) final public var strokeColor: UIKit::UIColor { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $strokeColor: Combine.Published.Publisher { + final public var $strokeColor: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($strokeWidth) final public var strokeWidth: Swift.Double { + @Combine::Published @_projectedValueProperty($strokeWidth) final public var strokeWidth: Swift::Double { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $strokeWidth: Combine.Published.Publisher { + final public var $strokeWidth: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($geodesic) final public var geodesic: Swift.Bool { + @Combine::Published @_projectedValueProperty($geodesic) final public var geodesic: Swift::Bool { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $geodesic: Combine.Published.Publisher { + final public var $geodesic: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published<[any MapConductorCore.GeoPointProtocol]> @_projectedValueProperty($points) final public var points: [any MapConductorCore.GeoPointProtocol] { + @Combine::Published<[any MapConductorCore::GeoPointProtocol]> @_projectedValueProperty($points) final public var points: [any MapConductorCore::GeoPointProtocol] { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $points: Combine.Published<[any MapConductorCore.GeoPointProtocol]>.Publisher { + final public var $points: Combine::Published<[any MapConductorCore::GeoPointProtocol]>.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($zIndex) final public var zIndex: Swift.Int { + @Combine::Published @_projectedValueProperty($zIndex) final public var zIndex: Swift::Int { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $zIndex: Combine.Published.Publisher { + final public var $zIndex: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($extra) final public var extra: Any? { + @Combine::Published @_projectedValueProperty($extra) final public var extra: Any? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $extra: Combine.Published.Publisher { + final public var $extra: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore.OnPolylineEventHandler? { + @Combine::Published @_projectedValueProperty($onClick) final public var onClick: MapConductorCore::OnPolylineEventHandler? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $onClick: Combine.Published.Publisher { + final public var $onClick: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - public init(points: [any MapConductorCore.GeoPointProtocol], id: Swift.String? = nil, strokeColor: UIKit.UIColor = .black, strokeWidth: Swift.Double = 1.0, geodesic: Swift.Bool = false, zIndex: Swift.Int = 0, extra: Any? = nil, onClick: MapConductorCore.OnPolylineEventHandler? = nil) - final public func copy(points: [any MapConductorCore.GeoPointProtocol]? = nil, id: Swift.String? = nil, strokeColor: UIKit.UIColor? = nil, strokeWidth: Swift.Double? = nil, geodesic: Swift.Bool? = nil, zIndex: Swift.Int? = nil, extra: Any? = nil, onClick: MapConductorCore.OnPolylineEventHandler? = nil) -> MapConductorCore.PolylineState - final public func fingerPrint() -> MapConductorCore.PolylineFingerPrint - final public func asFlow() -> Combine.AnyPublisher - public static func == (lhs: MapConductorCore.PolylineState, rhs: MapConductorCore.PolylineState) -> Swift.Bool - final public func hash(into hasher: inout Swift.Hasher) - final public func hashCode() -> Swift.Int - public typealias ID = Swift.String - public typealias ObjectWillChangePublisher = Combine.ObservableObjectPublisher + public init(points: [any MapConductorCore::GeoPointProtocol], id: Swift::String? = nil, strokeColor: UIKit::UIColor = .black, strokeWidth: Swift::Double = 1.0, geodesic: Swift::Bool = false, zIndex: Swift::Int = 0, extra: Any? = nil, onClick: MapConductorCore::OnPolylineEventHandler? = nil) + final public func copy(points: [any MapConductorCore::GeoPointProtocol]? = nil, id: Swift::String? = nil, strokeColor: UIKit::UIColor? = nil, strokeWidth: Swift::Double? = nil, geodesic: Swift::Bool? = nil, zIndex: Swift::Int? = nil, extra: Any? = nil, onClick: MapConductorCore::OnPolylineEventHandler? = nil) -> MapConductorCore::PolylineState + final public func fingerPrint() -> MapConductorCore::PolylineFingerPrint + final public func asFlow() -> Combine::AnyPublisher + public static func == (lhs: MapConductorCore::PolylineState, rhs: MapConductorCore::PolylineState) -> Swift::Bool + final public func hash(into hasher: inout Swift::Hasher) + final public func hashCode() -> Swift::Int + public typealias ID = Swift::String + public typealias ObjectWillChangePublisher = Combine::ObservableObjectPublisher @objc deinit - final public var hashValue: Swift.Int { + final public var hashValue: Swift::Int { get } } -public struct RasterHeaderRule : Swift.Equatable { - public let host: Swift.String - public let port: Swift.Int? - public let userAgent: Swift.String? - public let extraHeaders: [Swift.String : Swift.String] - public init(host: Swift.String, port: Swift.Int?, userAgent: Swift.String?, extraHeaders: [Swift.String : Swift.String]) - public static func == (a: MapConductorCore.RasterHeaderRule, b: MapConductorCore.RasterHeaderRule) -> Swift.Bool +public struct RasterHeaderRule : Swift::Equatable { + public let host: Swift::String + public let port: Swift::Int? + public let userAgent: Swift::String? + public let extraHeaders: [Swift::String : Swift::String] + public init(host: Swift::String, port: Swift::Int?, userAgent: Swift::String?, extraHeaders: [Swift::String : Swift::String]) + public static func == (a: MapConductorCore::RasterHeaderRule, b: MapConductorCore::RasterHeaderRule) -> Swift::Bool } -final public class RasterHeaderRuleSet : @unchecked Swift.Sendable { - public static let shared: MapConductorCore.RasterHeaderRuleSet +final public class RasterHeaderRuleSet : @unchecked Swift::Sendable { + public static let shared: MapConductorCore::RasterHeaderRuleSet public init() - final public var isEmpty: Swift.Bool { + final public var isEmpty: Swift::Bool { get } - final public func setRules(_ rules: [MapConductorCore.RasterHeaderRule], owner: Swift.AnyObject) - final public func removeRules(owner: Swift.AnyObject) - final public func headers(for url: Foundation.URL) -> (userAgent: Swift.String?, extraHeaders: [Swift.String : Swift.String])? - public static func makeRules(from states: [MapConductorCore.RasterLayerState]) -> [MapConductorCore.RasterHeaderRule] - public static func warnUnsupported(provider: Swift.String, state: MapConductorCore.RasterLayerState, supportsUserAgent: Swift.Bool = false) + final public func setRules(_ rules: [MapConductorCore::RasterHeaderRule], owner: Swift::AnyObject) + final public func removeRules(owner: Swift::AnyObject) + final public func headers(for url: Foundation::URL) -> (userAgent: Swift::String?, extraHeaders: [Swift::String : Swift::String])? + public static func makeRules(from states: [MapConductorCore::RasterLayerState]) -> [MapConductorCore::RasterHeaderRule] + public static func warnUnsupported(provider: Swift::String, state: MapConductorCore::RasterLayerState, supportsUserAgent: Swift::Bool = false) @objc deinit } -open class RasterLayerController : MapConductorCore.OverlayControllerProtocol, MapConductorCore.SlottedOverlayController where ActualLayer == Renderer.ActualLayer, Renderer : MapConductorCore.RasterLayerOverlayRendererProtocol { - public typealias StateType = MapConductorCore.RasterLayerState - public typealias EntityType = MapConductorCore.RasterLayerEntity - public typealias EventType = MapConductorCore.RasterLayerEvent - final public let rasterLayerManager: MapConductorCore.RasterLayerManager +open class RasterLayerController : MapConductorCore::OverlayControllerProtocol, MapConductorCore::SlottedOverlayController, MapConductorCore::RasterLayerMounting where ActualLayer == Renderer.ActualLayer, Renderer : MapConductorCore::RasterLayerOverlayRendererProtocol { + public typealias StateType = MapConductorCore::RasterLayerState + public typealias EntityType = MapConductorCore::RasterLayerEntity + public typealias EventType = MapConductorCore::RasterLayerEvent + final public let rasterLayerManager: MapConductorCore::RasterLayerManager open var renderer: Renderer - final public let zIndex: Swift.Int - public var clickListener: ((MapConductorCore.RasterLayerEvent) -> Swift.Void)? - public init(rasterLayerManager: MapConductorCore.RasterLayerManager, renderer: Renderer, clickListener: ((MapConductorCore.RasterLayerEvent) -> Swift.Void)? = nil) - public func dispatchClick(event: MapConductorCore.RasterLayerEvent) - open func add(data: [MapConductorCore.RasterLayerState]) async - open func update(state: MapConductorCore.RasterLayerState) async - open func upsert(state: MapConductorCore.RasterLayerState) async - open func removeById(_ id: Swift.String) async + final public let zIndex: Swift::Int + public var clickListener: ((MapConductorCore::RasterLayerEvent) -> Swift::Void)? + public init(rasterLayerManager: MapConductorCore::RasterLayerManager, renderer: Renderer, clickListener: ((MapConductorCore::RasterLayerEvent) -> Swift::Void)? = nil) + public func dispatchClick(event: MapConductorCore::RasterLayerEvent) + open func add(data: [MapConductorCore::RasterLayerState]) async + open func update(state: MapConductorCore::RasterLayerState) async + open func upsert(state: MapConductorCore::RasterLayerState) async + open func removeById(_ id: Swift::String) async open func clear() async - open func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.RasterLayerEntity? - open func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + open func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::RasterLayerEntity? + open func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async open func destroy() - public var kind: MapConductorCore.OverlayKind { + public var kind: MapConductorCore::OverlayKind { get } - public func hasId(_ id: Swift.String) -> Swift.Bool - public func resolveTap(position _: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.OverlayHit? + public func hasId(_ id: Swift::String) -> Swift::Bool + public func resolveTap(position _: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::OverlayHit? @objc deinit } public protocol RasterLayerEntityProtocol { associatedtype ActualLayer var layer: Self.ActualLayer? { get set } - var state: MapConductorCore.RasterLayerState { get } - var fingerPrint: MapConductorCore.RasterLayerFingerPrint { get } + var state: MapConductorCore::RasterLayerState { get } + var fingerPrint: MapConductorCore::RasterLayerFingerPrint { get } } -final public class RasterLayerEntity : MapConductorCore.RasterLayerEntityProtocol { +final public class RasterLayerEntity : MapConductorCore::RasterLayerEntityProtocol { final public var layer: ActualLayer? - final public let state: MapConductorCore.RasterLayerState - final public let fingerPrint: MapConductorCore.RasterLayerFingerPrint - public init(layer: ActualLayer?, state: MapConductorCore.RasterLayerState) + final public let state: MapConductorCore::RasterLayerState + final public let fingerPrint: MapConductorCore::RasterLayerFingerPrint + public init(layer: ActualLayer?, state: MapConductorCore::RasterLayerState) @objc deinit } public protocol RasterLayerManagerProtocol { associatedtype ActualLayer - func registerEntity(_ entity: MapConductorCore.RasterLayerEntity) - func removeEntity(_ id: Swift.String) -> MapConductorCore.RasterLayerEntity? - func getEntity(_ id: Swift.String) -> MapConductorCore.RasterLayerEntity? - func hasEntity(_ id: Swift.String) -> Swift.Bool - func allEntities() -> [MapConductorCore.RasterLayerEntity] + func registerEntity(_ entity: MapConductorCore::RasterLayerEntity) + func removeEntity(_ id: Swift::String) -> MapConductorCore::RasterLayerEntity? + func getEntity(_ id: Swift::String) -> MapConductorCore::RasterLayerEntity? + func hasEntity(_ id: Swift::String) -> Swift::Bool + func allEntities() -> [MapConductorCore::RasterLayerEntity] func clear() - func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.RasterLayerEntity? + func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::RasterLayerEntity? } -final public class RasterLayerManager : MapConductorCore.RasterLayerManagerProtocol { +final public class RasterLayerManager : MapConductorCore::RasterLayerManagerProtocol { public init() - final public func registerEntity(_ entity: MapConductorCore.RasterLayerEntity) - final public func removeEntity(_ id: Swift.String) -> MapConductorCore.RasterLayerEntity? - final public func getEntity(_ id: Swift.String) -> MapConductorCore.RasterLayerEntity? - final public func hasEntity(_ id: Swift.String) -> Swift.Bool - final public func allEntities() -> [MapConductorCore.RasterLayerEntity] + final public func registerEntity(_ entity: MapConductorCore::RasterLayerEntity) + final public func removeEntity(_ id: Swift::String) -> MapConductorCore::RasterLayerEntity? + final public func getEntity(_ id: Swift::String) -> MapConductorCore::RasterLayerEntity? + final public func hasEntity(_ id: Swift::String) -> Swift::Bool + final public func allEntities() -> [MapConductorCore::RasterLayerEntity] final public func clear() final public func destroy() - final public var isDestroyed: Swift.Bool { + final public var isDestroyed: Swift::Bool { get } - final public func find(position: any MapConductorCore.GeoPointProtocol) -> MapConductorCore.RasterLayerEntity? + final public func find(position: any MapConductorCore::GeoPointProtocol) -> MapConductorCore::RasterLayerEntity? @objc deinit } public protocol RasterLayerOverlayRendererProtocol { associatedtype ActualLayer - func onAdd(data: [MapConductorCore.RasterLayerOverlayAddParams]) async -> [Self.ActualLayer?] - func onChange(data: [MapConductorCore.RasterLayerOverlayChangeParams]) async -> [Self.ActualLayer?] - func onRemove(data: [MapConductorCore.RasterLayerEntity]) async - func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async + func onAdd(data: [MapConductorCore::RasterLayerOverlayAddParams]) async -> [Self.ActualLayer?] + func onChange(data: [MapConductorCore::RasterLayerOverlayChangeParams]) async -> [Self.ActualLayer?] + func onRemove(data: [MapConductorCore::RasterLayerEntity]) async + func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async func onPostProcess() async } -extension MapConductorCore.RasterLayerOverlayRendererProtocol { - public func onCameraChanged(mapCameraPosition: MapConductorCore.MapCameraPosition) async +extension MapConductorCore::RasterLayerOverlayRendererProtocol { + public func onCameraChanged(mapCameraPosition: MapConductorCore::MapCameraPosition) async } -open class AbstractRasterLayerOverlayRenderer : MapConductorCore.RasterLayerOverlayRendererProtocol { +open class AbstractRasterLayerOverlayRenderer : MapConductorCore::RasterLayerOverlayRendererProtocol { public init() open func onPostProcess() async - open func createLayer(state: MapConductorCore.RasterLayerState) async -> ActualLayer? - open func updateLayerProperties(layer: ActualLayer, current: MapConductorCore.RasterLayerEntity, prev: MapConductorCore.RasterLayerEntity) async -> ActualLayer? - open func removeLayer(entity: MapConductorCore.RasterLayerEntity) async - public func onAdd(data: [MapConductorCore.RasterLayerOverlayAddParams]) async -> [ActualLayer?] - public func onChange(data: [MapConductorCore.RasterLayerOverlayChangeParams]) async -> [ActualLayer?] - public func onRemove(data: [MapConductorCore.RasterLayerEntity]) async + open func createLayer(state: MapConductorCore::RasterLayerState) async -> ActualLayer? + open func updateLayerProperties(layer: ActualLayer, current: MapConductorCore::RasterLayerEntity, prev: MapConductorCore::RasterLayerEntity) async -> ActualLayer? + open func removeLayer(entity: MapConductorCore::RasterLayerEntity) async + public func onAdd(data: [MapConductorCore::RasterLayerOverlayAddParams]) async -> [ActualLayer?] + public func onChange(data: [MapConductorCore::RasterLayerOverlayChangeParams]) async -> [ActualLayer?] + public func onRemove(data: [MapConductorCore::RasterLayerEntity]) async @objc deinit } public struct RasterLayerOverlayAddParams { - public let state: MapConductorCore.RasterLayerState - public init(state: MapConductorCore.RasterLayerState) + public let state: MapConductorCore::RasterLayerState + public init(state: MapConductorCore::RasterLayerState) } public struct RasterLayerOverlayChangeParams { - public let current: MapConductorCore.RasterLayerEntity - public let prev: MapConductorCore.RasterLayerEntity - public init(current: MapConductorCore.RasterLayerEntity, prev: MapConductorCore.RasterLayerEntity) + public let current: MapConductorCore::RasterLayerEntity + public let prev: MapConductorCore::RasterLayerEntity + public init(current: MapConductorCore::RasterLayerEntity, prev: MapConductorCore::RasterLayerEntity) } -public enum TileScheme : Swift.String, Swift.Hashable { +public enum TileScheme : Swift::String, Swift::Hashable { case XYZ case TMS - public init?(rawValue: Swift.String) - public typealias RawValue = Swift.String - public var rawValue: Swift.String { + public init?(rawValue: Swift::String) + public typealias RawValue = Swift::String + public var rawValue: Swift::String { get } } -public enum RasterLayerSource : Swift.Hashable { - case urlTemplate(template: Swift.String, tileSize: Swift.Int = RasterLayerSource.defaultTileSize, minZoom: Swift.Int? = nil, maxZoom: Swift.Int? = nil, attributionRules: [MapConductorCore.AttributionRule] = [], scheme: MapConductorCore.TileScheme = .XYZ) - case tileJson(url: Swift.String) - case arcGisService(serviceUrl: Swift.String) - public static let defaultTileSize: Swift.Int - public static func == (a: MapConductorCore.RasterLayerSource, b: MapConductorCore.RasterLayerSource) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public enum RasterLayerSource : Swift::Hashable { + case urlTemplate(template: Swift::String, tileSize: Swift::Int = RasterLayerSource.defaultTileSize, minZoom: Swift::Int? = nil, maxZoom: Swift::Int? = nil, attributionRules: [MapConductorCore::AttributionRule] = [], scheme: MapConductorCore::TileScheme = .XYZ) + case tileJson(url: Swift::String) + case arcGisService(serviceUrl: Swift::String) + public static let defaultTileSize: Swift::Int + public static func == (a: MapConductorCore::RasterLayerSource, b: MapConductorCore::RasterLayerSource) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } -public struct RasterLayerFingerPrint : Swift.Equatable, Swift.Hashable { - public let id: Swift.Int - public let source: Swift.Int - public let opacity: Swift.Int - public let visible: Swift.Int - public let zIndex: Swift.Int - public let debug: Swift.Int - public let userAgent: Swift.Int - public let extraHeaders: Swift.Int - public static func == (a: MapConductorCore.RasterLayerFingerPrint, b: MapConductorCore.RasterLayerFingerPrint) -> Swift.Bool - public func hash(into hasher: inout Swift.Hasher) - public var hashValue: Swift.Int { +public struct RasterLayerFingerPrint : Swift::Equatable, Swift::Hashable { + public let id: Swift::Int + public let source: Swift::Int + public let opacity: Swift::Int + public let visible: Swift::Int + public let zIndex: Swift::Int + public let debug: Swift::Int + public let userAgent: Swift::Int + public let extraHeaders: Swift::Int + public static func == (a: MapConductorCore::RasterLayerFingerPrint, b: MapConductorCore::RasterLayerFingerPrint) -> Swift::Bool + public func hash(into hasher: inout Swift::Hasher) + public var hashValue: Swift::Int { get } } public struct RasterLayerEvent { - public let state: MapConductorCore.RasterLayerState - public init(state: MapConductorCore.RasterLayerState) + public let state: MapConductorCore::RasterLayerState + public init(state: MapConductorCore::RasterLayerState) } -public typealias OnRasterLayerEventHandler = (MapConductorCore.RasterLayerEvent) -> Swift.Void -final public class RasterLayerState : Combine.ObservableObject, Swift.Identifiable, Swift.Equatable, Swift.Hashable { - public static let defaultUserAgent: Swift.String - final public let id: Swift.String - @Combine.Published @_projectedValueProperty($source) final public var source: MapConductorCore.RasterLayerSource { +public typealias OnRasterLayerEventHandler = (MapConductorCore::RasterLayerEvent) -> Swift::Void +final public class RasterLayerState : Combine::ObservableObject, Swift::Identifiable, Swift::Equatable, Swift::Hashable { + public static let defaultUserAgent: Swift::String + final public let id: Swift::String + final public let mutations: MapConductorCore::StateMutationSignal + @Combine::Published @_projectedValueProperty($source) final public var source: MapConductorCore::RasterLayerSource { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $source: Combine.Published.Publisher { + final public var $source: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($opacity) final public var opacity: Swift.Double { + @Combine::Published @_projectedValueProperty($opacity) final public var opacity: Swift::Double { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $opacity: Combine.Published.Publisher { + final public var $opacity: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($visible) final public var visible: Swift.Bool { + @Combine::Published @_projectedValueProperty($visible) final public var visible: Swift::Bool { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $visible: Combine.Published.Publisher { + final public var $visible: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($zIndex) final public var zIndex: Swift.Int { + @Combine::Published @_projectedValueProperty($zIndex) final public var zIndex: Swift::Int { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $zIndex: Combine.Published.Publisher { + final public var $zIndex: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($debug) final public var debug: Swift.Bool { + @Combine::Published @_projectedValueProperty($debug) final public var debug: Swift::Bool { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $debug: Combine.Published.Publisher { + final public var $debug: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published @_projectedValueProperty($userAgent) final public var userAgent: Swift.String? { + @Combine::Published @_projectedValueProperty($userAgent) final public var userAgent: Swift::String? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $userAgent: Combine.Published.Publisher { + final public var $userAgent: Combine::Published.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - @Combine.Published<[Swift.String : Swift.String]?> @_projectedValueProperty($extraHeaders) final public var extraHeaders: [Swift.String : Swift.String]? { + @Combine::Published<[Swift::String : Swift::String]?> @_projectedValueProperty($extraHeaders) final public var extraHeaders: [Swift::String : Swift::String]? { get - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) set - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - _modify } - final public var $extraHeaders: Combine.Published<[Swift.String : Swift.String]?>.Publisher { + final public var $extraHeaders: Combine::Published<[Swift::String : Swift::String]?>.Combine::Publisher { get - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - public init(source: MapConductorCore.RasterLayerSource, opacity: Swift.Double = 1.0, visible: Swift.Bool = true, zIndex: Swift.Int = 0, debug: Swift.Bool = false, userAgent: Swift.String = RasterLayerState.defaultUserAgent, extraHeaders: [Swift.String : Swift.String]? = nil, id: Swift.String? = nil) - final public func copy(source: MapConductorCore.RasterLayerSource? = nil, opacity: Swift.Double? = nil, visible: Swift.Bool? = nil, zIndex: Swift.Int? = nil, debug: Swift.Bool? = nil, userAgent: Swift.String? = nil, extraHeaders: [Swift.String : Swift.String]? = nil, id: Swift.String? = nil) -> MapConductorCore.RasterLayerState - final public func fingerPrint() -> MapConductorCore.RasterLayerFingerPrint - final public func asFlow() -> Combine.AnyPublisher - public static func == (lhs: MapConductorCore.RasterLayerState, rhs: MapConductorCore.RasterLayerState) -> Swift.Bool - final public func hash(into hasher: inout Swift.Hasher) - final public func hashCode() -> Swift.Int - public typealias ID = Swift.String - public typealias ObjectWillChangePublisher = Combine.ObservableObjectPublisher + public init(source: MapConductorCore::RasterLayerSource, opacity: Swift::Double = 1.0, visible: Swift::Bool = true, zIndex: Swift::Int = 0, debug: Swift::Bool = false, userAgent: Swift::String = RasterLayerState.defaultUserAgent, extraHeaders: [Swift::String : Swift::String]? = nil, id: Swift::String? = nil) + final public func copy(source: MapConductorCore::RasterLayerSource? = nil, opacity: Swift::Double? = nil, visible: Swift::Bool? = nil, zIndex: Swift::Int? = nil, debug: Swift::Bool? = nil, userAgent: Swift::String? = nil, extraHeaders: [Swift::String : Swift::String]? = nil, id: Swift::String? = nil) -> MapConductorCore::RasterLayerState + final public func fingerPrint() -> MapConductorCore::RasterLayerFingerPrint + final public func asFlow() -> Combine::AnyPublisher + public static func == (lhs: MapConductorCore::RasterLayerState, rhs: MapConductorCore::RasterLayerState) -> Swift::Bool + final public func hash(into hasher: inout Swift::Hasher) + final public func hashCode() -> Swift::Int + public typealias ID = Swift::String + public typealias ObjectWillChangePublisher = Combine::ObservableObjectPublisher @objc deinit - final public var hashValue: Swift.Int { + final public var hashValue: Swift::Int { + get + } +} +public protocol RasterTilePreference { + var preferredTileSize: Swift::Int { get } + var layoutInDevicePixels: Swift::Bool { get } +} +extension MapConductorCore::RasterTilePreference { + public var layoutInDevicePixels: Swift::Bool { get } } +public enum RasterTilePreferenceKey : MapConductorCore::MapServiceKey { + public typealias Value = MapConductorCore::RasterTilePreference +} +public struct FixedRasterTilePreference : MapConductorCore::RasterTilePreference { + public let preferredTileSize: Swift::Int + public let layoutInDevicePixels: Swift::Bool + public init(preferredTileSize: Swift::Int, layoutInDevicePixels: Swift::Bool = false) +} public enum MarkerIconSize { - public static let Small: CoreFoundation.CGFloat - public static let Regular: CoreFoundation.CGFloat - public static let Large: CoreFoundation.CGFloat + public static let Small: CoreFoundation::CGFloat + public static let Regular: CoreFoundation::CGFloat + public static let Large: CoreFoundation::CGFloat } public struct Settings { - public let tapTolerance: CoreFoundation.CGFloat - public let markerDropAnimateDuration: Swift.Int - public let markerBounceAnimateDuration: Swift.Int - public let iconSize: CoreFoundation.CGFloat - public let iconStroke: CoreFoundation.CGFloat - public let composeEventDebounce: Swift.Int - public init(tapTolerance: CoreFoundation.CGFloat, markerDropAnimateDuration: Swift.Int, markerBounceAnimateDuration: Swift.Int, iconSize: CoreFoundation.CGFloat, iconStroke: CoreFoundation.CGFloat, composeEventDebounce: Swift.Int) - public static let Default: MapConductorCore.Settings + public let tapTolerance: CoreFoundation::CGFloat + public let markerDropAnimateDuration: Swift::Int + public let markerBounceAnimateDuration: Swift::Int + public let iconSize: CoreFoundation::CGFloat + public let iconStroke: CoreFoundation::CGFloat + public let composeEventDebounce: Swift::Int + public init(tapTolerance: CoreFoundation::CGFloat, markerDropAnimateDuration: Swift::Int, markerBounceAnimateDuration: Swift::Int, iconSize: CoreFoundation::CGFloat, iconStroke: CoreFoundation::CGFloat, composeEventDebounce: Swift::Int) + public static let Default: MapConductorCore::Settings } -extension MapConductorCore.Settings { - public var markerDropAnimateInterval: Foundation.TimeInterval { +extension MapConductorCore::Settings { + public var markerDropAnimateInterval: Foundation::TimeInterval { get } - public var markerBounceAnimateInterval: Foundation.TimeInterval { + public var markerBounceAnimateInterval: Foundation::TimeInterval { get } } -public func calculateMetersPerPixel(latitude: Swift.Double, zoom: Swift.Double, tileSize: Swift.Double = 256.0) -> Swift.Double -public func closestPointOnSegment(startPoint: CoreFoundation.CGPoint, endPoint: CoreFoundation.CGPoint, testPoint: CoreFoundation.CGPoint) -> CoreFoundation.CGPoint -public func expandBounds(bounds: MapConductorCore.GeoRectBounds, margin: Swift.Double) -> MapConductorCore.GeoRectBounds +public func calculateMetersPerPixel(latitude: Swift::Double, zoom: Swift::Double, tileSize: Swift::Double = 256.0) -> Swift::Double +public func closestPointOnSegment(startPoint: CoreFoundation::CGPoint, endPoint: CoreFoundation::CGPoint, testPoint: CoreFoundation::CGPoint) -> CoreFoundation::CGPoint +public func expandBounds(bounds: MapConductorCore::GeoRectBounds, margin: Swift::Double) -> MapConductorCore::GeoRectBounds public enum Planar { - public static func interpolate(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol, fraction: Swift.Double) -> MapConductorCore.GeoPoint - public static func createInterpolatePoints(_ points: [any MapConductorCore.GeoPointProtocol], maxSegmentLength: Swift.Double = 10_000.0) -> [any MapConductorCore.GeoPointProtocol] - public static func pointOnLineOrNull(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol, position: any MapConductorCore.GeoPointProtocol, thresholdMeters: Swift.Double) -> (any MapConductorCore.GeoPointProtocol, Swift.Double)? + public static func interpolate(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol, fraction: Swift::Double) -> MapConductorCore::GeoPoint + public static func createInterpolatePoints(_ points: [any MapConductorCore::GeoPointProtocol], maxSegmentLength: Swift::Double = 10_000.0) -> [any MapConductorCore::GeoPointProtocol] + public static func pointOnLineOrNull(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol, position: any MapConductorCore::GeoPointProtocol, thresholdMeters: Swift::Double) -> (any MapConductorCore::GeoPointProtocol, Swift::Double)? } public enum Spherical { - public static func computeDistanceBetween(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol) -> Swift.Double - public static func computeHeading(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol) -> Swift.Double - public static func computeOffset(origin: any MapConductorCore.GeoPointProtocol, distance: Swift.Double, heading: Swift.Double) -> MapConductorCore.GeoPoint - public static func computeOffsetOrigin(to: any MapConductorCore.GeoPointProtocol, distance: Swift.Double, heading: Swift.Double) -> MapConductorCore.GeoPoint? - public static func computeLength(_ path: [any MapConductorCore.GeoPointProtocol]) -> Swift.Double - public static func computeArea(_ path: [any MapConductorCore.GeoPointProtocol]) -> Swift.Double - public static func computeSignedArea(_ path: [any MapConductorCore.GeoPointProtocol]) -> Swift.Double - public static func interpolate(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol, fraction: Swift.Double) -> MapConductorCore.GeoPoint -} -public func splitByMeridian(_ points: [any MapConductorCore.GeoPointProtocol], geodesic: Swift.Bool) -> [[any MapConductorCore.GeoPointProtocol]] + public static func computeDistanceBetween(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol) -> Swift::Double + public static func computeHeading(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol) -> Swift::Double + public static func computeOffset(origin: any MapConductorCore::GeoPointProtocol, distance: Swift::Double, heading: Swift::Double) -> MapConductorCore::GeoPoint + public static func computeOffsetOrigin(to: any MapConductorCore::GeoPointProtocol, distance: Swift::Double, heading: Swift::Double) -> MapConductorCore::GeoPoint? + public static func computeLength(_ path: [any MapConductorCore::GeoPointProtocol]) -> Swift::Double + public static func computeArea(_ path: [any MapConductorCore::GeoPointProtocol]) -> Swift::Double + public static func computeSignedArea(_ path: [any MapConductorCore::GeoPointProtocol]) -> Swift::Double + public static func interpolate(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol, fraction: Swift::Double) -> MapConductorCore::GeoPoint +} +public func splitByMeridian(_ points: [any MapConductorCore::GeoPointProtocol], geodesic: Swift::Bool) -> [[any MapConductorCore::GeoPointProtocol]] public enum WGS84Geodesic { - public static func computeDistanceBetween(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol) -> Swift.Double - public static func computeHeading(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol) -> Swift.Double - public static func computeOffset(origin: any MapConductorCore.GeoPointProtocol, distance: Swift.Double, heading: Swift.Double) -> MapConductorCore.GeoPoint - public static func computeOffsetOrigin(to: any MapConductorCore.GeoPointProtocol, distance: Swift.Double, heading: Swift.Double) -> MapConductorCore.GeoPoint? - public static func computeLength(_ path: [any MapConductorCore.GeoPointProtocol]) -> Swift.Double - public static func createInterpolatePoints(_ points: [any MapConductorCore.GeoPointProtocol], maxSegmentLength: Swift.Double = 10_000.0) -> [any MapConductorCore.GeoPointProtocol] - public static func pointOnLineOrNull(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol, position: any MapConductorCore.GeoPointProtocol, thresholdMeters: Swift.Double) -> (any MapConductorCore.GeoPointProtocol, Swift.Double)? - public static func computeSignedArea(_ path: [any MapConductorCore.GeoPointProtocol]) -> Swift.Double - public static func computeArea(_ path: [any MapConductorCore.GeoPointProtocol]) -> Swift.Double - public static func interpolate(from: any MapConductorCore.GeoPointProtocol, to: any MapConductorCore.GeoPointProtocol, fraction: Swift.Double) -> MapConductorCore.GeoPoint + public static func computeDistanceBetween(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol) -> Swift::Double + public static func computeHeading(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol) -> Swift::Double + public static func computeOffset(origin: any MapConductorCore::GeoPointProtocol, distance: Swift::Double, heading: Swift::Double) -> MapConductorCore::GeoPoint + public static func computeOffsetOrigin(to: any MapConductorCore::GeoPointProtocol, distance: Swift::Double, heading: Swift::Double) -> MapConductorCore::GeoPoint? + public static func computeLength(_ path: [any MapConductorCore::GeoPointProtocol]) -> Swift::Double + public static func createInterpolatePoints(_ points: [any MapConductorCore::GeoPointProtocol], maxSegmentLength: Swift::Double = 10_000.0) -> [any MapConductorCore::GeoPointProtocol] + public static func pointOnLineOrNull(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol, position: any MapConductorCore::GeoPointProtocol, thresholdMeters: Swift::Double) -> (any MapConductorCore::GeoPointProtocol, Swift::Double)? + public static func computeSignedArea(_ path: [any MapConductorCore::GeoPointProtocol]) -> Swift::Double + public static func computeArea(_ path: [any MapConductorCore::GeoPointProtocol]) -> Swift::Double + public static func interpolate(from: any MapConductorCore::GeoPointProtocol, to: any MapConductorCore::GeoPointProtocol, fraction: Swift::Double) -> MapConductorCore::GeoPoint +} +public enum TilePngEncoder { + public static func encode(_ image: UIKit::UIImage) -> Foundation::Data? + public static func encode(rgba: Swift::UnsafeMutableRawPointer, width: Swift::Int, height: Swift::Int, premultiplied: Swift::Bool) -> Foundation::Data? } public enum AbstractZoomAltitudeConverter { - public static let defaultZoom0Altitude: Swift.Double - public static let zoomFactor: Swift.Double - public static let minZoomLevel: Swift.Double - public static let maxZoomLevel: Swift.Double - public static let minAltitude: Swift.Double - public static let maxAltitude: Swift.Double - public static let minCosLat: Swift.Double - public static let minCosTilt: Swift.Double - public static let webMercatorInitialMpp256: Swift.Double -} -open class WebMercatorZoomAltitudeConverter : MapConductorCore.ZoomAltitudeConverterProtocol { - final public let zoom0Altitude: Swift.Double - public init(zoom0Altitude: Swift.Double = AbstractZoomAltitudeConverter.defaultZoom0Altitude, zoomOffset: Swift.Double = 0.0) - open func zoomOffset(at _: Swift.Double) -> Swift.Double - public func toUnifiedZoom(_ nativeZoom: Swift.Double, latitude: Swift.Double = 0.0) -> Swift.Double - public func toNativeZoom(_ unifiedZoom: Swift.Double, latitude: Swift.Double = 0.0) -> Swift.Double - public func cosLatitudeFactor(_ latitudeDeg: Swift.Double) -> Swift.Double - public func cosTiltFactor(_ tiltDeg: Swift.Double) -> Swift.Double - public func zoomLevelToAltitude(zoomLevel: Swift.Double, latitude: Swift.Double, tilt: Swift.Double) -> Swift.Double - public func altitudeToZoomLevel(altitude: Swift.Double, latitude: Swift.Double, tilt: Swift.Double) -> Swift.Double - @objc deinit -} -open class GroundScaleZoomAltitudeConverter : MapConductorCore.WebMercatorZoomAltitudeConverter { - public init(zoom0Altitude: Swift.Double = AbstractZoomAltitudeConverter.defaultZoom0Altitude, baseZoomOffset: Swift.Double) - override public func zoomOffset(at latitude: Swift.Double) -> Swift.Double + public static let defaultZoom0Altitude: Swift::Double + public static let zoomFactor: Swift::Double + public static let minZoomLevel: Swift::Double + public static let maxZoomLevel: Swift::Double + public static let minAltitude: Swift::Double + public static let maxAltitude: Swift::Double + public static let minCosLat: Swift::Double + public static let minCosTilt: Swift::Double + public static let webMercatorInitialMpp256: Swift::Double +} +open class WebMercatorZoomAltitudeConverter : MapConductorCore::ZoomAltitudeConverterProtocol { + final public let zoom0Altitude: Swift::Double + public init(zoom0Altitude: Swift::Double = AbstractZoomAltitudeConverter.defaultZoom0Altitude, zoomOffset: Swift::Double = 0.0) + open func zoomOffset(at _: Swift::Double) -> Swift::Double + public func toUnifiedZoom(_ nativeZoom: Swift::Double, latitude: Swift::Double = 0.0) -> Swift::Double + public func toNativeZoom(_ unifiedZoom: Swift::Double, latitude: Swift::Double = 0.0) -> Swift::Double + public func cosLatitudeFactor(_ latitudeDeg: Swift::Double) -> Swift::Double + public func cosTiltFactor(_ tiltDeg: Swift::Double) -> Swift::Double + public func zoomLevelToAltitude(zoomLevel: Swift::Double, latitude: Swift::Double, tilt: Swift::Double) -> Swift::Double + public func altitudeToZoomLevel(altitude: Swift::Double, latitude: Swift::Double, tilt: Swift::Double) -> Swift::Double + @objc deinit +} +open class GroundScaleZoomAltitudeConverter : MapConductorCore::WebMercatorZoomAltitudeConverter { + public init(zoom0Altitude: Swift::Double = AbstractZoomAltitudeConverter.defaultZoom0Altitude, baseZoomOffset: Swift::Double) + override public func zoomOffset(at latitude: Swift::Double) -> Swift::Double @objc deinit } public protocol ZoomAltitudeConverterProtocol { - var zoom0Altitude: Swift.Double { get } - func zoomLevelToAltitude(zoomLevel: Swift.Double, latitude: Swift.Double, tilt: Swift.Double) -> Swift.Double - func altitudeToZoomLevel(altitude: Swift.Double, latitude: Swift.Double, tilt: Swift.Double) -> Swift.Double + var zoom0Altitude: Swift::Double { get } + func zoomLevelToAltitude(zoomLevel: Swift::Double, latitude: Swift::Double, tilt: Swift::Double) -> Swift::Double + func altitudeToZoomLevel(altitude: Swift::Double, latitude: Swift::Double, tilt: Swift::Double) -> Swift::Double } -extension MapConductorCore.ZoomAltitudeConverterProtocol { - public static var defaultZoom0Altitude: Swift.Double { +extension MapConductorCore::ZoomAltitudeConverterProtocol { + public static var defaultZoom0Altitude: Swift::Double { get } - public static var zoomFactor: Swift.Double { + public static var zoomFactor: Swift::Double { get } - public static var minZoomLevel: Swift.Double { + public static var minZoomLevel: Swift::Double { get } - public static var maxZoomLevel: Swift.Double { + public static var maxZoomLevel: Swift::Double { get } - public static var minAltitude: Swift.Double { + public static var minAltitude: Swift::Double { get } - public static var maxAltitude: Swift.Double { + public static var maxAltitude: Swift::Double { get } - public static var minCosLat: Swift.Double { + public static var minCosLat: Swift::Double { get } - public static var minCosTilt: Swift.Double { + public static var minCosTilt: Swift::Double { get } - public static var webMercatorInitialMpp256: Swift.Double { + public static var webMercatorInitialMpp256: Swift::Double { get } } -extension MapConductorCore.MapGesture : Swift.Equatable {} -extension MapConductorCore.MapGesture : Swift.Hashable {} -extension MapConductorCore.MapGesture : Swift.RawRepresentable {} -extension MapConductorCore.MapViewBase : Swift.Sendable {} -extension MapConductorCore.MapViewCoordinatorBase : Swift.Sendable {} -extension MapConductorCore.OverlayKind : Swift.Equatable {} -extension MapConductorCore.OverlayKind : Swift.Hashable {} -extension MapConductorCore.OverlayKind : Swift.RawRepresentable {} -extension MapConductorCore.Direction6 : Swift.Equatable {} -extension MapConductorCore.Direction6 : Swift.Hashable {} -extension MapConductorCore.DefaultInfoBubbleView : Swift.Sendable {} -extension MapConductorCore.InfoBubbleOverlayCoordinator : Swift.Sendable {} -extension MapConductorCore.MapAttributionOverlay : Swift.Sendable {} -extension MapConductorCore.MapCapability : Swift.Equatable {} -extension MapConductorCore.MapCapability : Swift.Hashable {} -extension MapConductorCore.MapCapability : Swift.RawRepresentable {} -extension MapConductorCore.MapDiagnosticLevel : Swift.Equatable {} -extension MapConductorCore.MapDiagnosticLevel : Swift.Hashable {} -extension MapConductorCore.MapDiagnosticLevel : Swift.RawRepresentable {} -extension MapConductorCore.MapServiceRegistryScope : Swift.Sendable {} -extension MapConductorCore.InitState : Swift.Equatable {} -extension MapConductorCore.InitState : Swift.Hashable {} -extension MapConductorCore.DefaultMarkerEventController : Swift.Sendable {} -extension MapConductorCore.MarkerDragGestureState : Swift.Equatable {} -extension MapConductorCore.MarkerDragGestureState : Swift.Hashable {} -extension MapConductorCore.MarkerAnimationOverlayCoordinator : Swift.Sendable {} -extension MapConductorCore.AnyMarkerOverlayRenderer : Swift.Sendable {} -extension MapConductorCore.MarkerAnimation : Swift.Equatable {} -extension MapConductorCore.MarkerAnimation : Swift.Hashable {} -extension MapConductorCore.StrategyMarkerManager : Swift.Sendable {} -extension MapConductorCore.DeferredUntilReady : Swift.Sendable {} -extension MapConductorCore.MapOverlayScope : Swift.Sendable {} -extension MapConductorCore.OverlayCollector : Swift.Sendable {} -extension MapConductorCore.TileScheme : Swift.RawRepresentable {} +extension MapConductorCore::MapGesture : Swift::Equatable {} +extension MapConductorCore::MapGesture : Swift::Hashable {} +extension MapConductorCore::MapGesture : Swift::RawRepresentable {} +extension MapConductorCore::MapViewBase : Swift::Sendable {} +extension MapConductorCore::MapViewCoordinatorBase : Swift::Sendable {} +extension MapConductorCore::OverlayKind : Swift::Equatable {} +extension MapConductorCore::OverlayKind : Swift::Hashable {} +extension MapConductorCore::OverlayKind : Swift::RawRepresentable {} +extension MapConductorCore::Direction6 : Swift::Equatable {} +extension MapConductorCore::Direction6 : Swift::Hashable {} +extension MapConductorCore::DefaultInfoBubbleView : Swift::Sendable {} +extension MapConductorCore::InfoBubbleOverlayCoordinator : Swift::Sendable {} +extension MapConductorCore::MapAttributionOverlay : Swift::Sendable {} +extension MapConductorCore::MapCapability : Swift::Equatable {} +extension MapConductorCore::MapCapability : Swift::Hashable {} +extension MapConductorCore::MapCapability : Swift::RawRepresentable {} +extension MapConductorCore::MapDiagnosticLevel : Swift::Equatable {} +extension MapConductorCore::MapDiagnosticLevel : Swift::Hashable {} +extension MapConductorCore::MapDiagnosticLevel : Swift::RawRepresentable {} +extension MapConductorCore::MapServiceRegistryScope : Swift::Sendable {} +extension MapConductorCore::InitState : Swift::Equatable {} +extension MapConductorCore::InitState : Swift::Hashable {} +extension MapConductorCore::MapViewStyleApplier : Swift::Sendable {} +extension MapConductorCore::DefaultMarkerEventController : Swift::Sendable {} +extension MapConductorCore::MarkerDragGestureState : Swift::Equatable {} +extension MapConductorCore::MarkerDragGestureState : Swift::Hashable {} +extension MapConductorCore::MarkerAnimationOverlayCoordinator : Swift::Sendable {} +extension MapConductorCore::AnyMarkerOverlayRenderer : Swift::Sendable {} +extension MapConductorCore::MarkerAnimation : Swift::Equatable {} +extension MapConductorCore::MarkerAnimation : Swift::Hashable {} +extension MapConductorCore::StrategyMarkerManager : Swift::Sendable {} +extension MapConductorCore::DeferredUntilReady : Swift::Sendable {} +extension MapConductorCore::MapOverlayScope : Swift::Sendable {} +extension MapConductorCore::OverlayCollector : Swift::Sendable {} +extension MapConductorCore::TileScheme : Swift::RawRepresentable {} diff --git a/api/TOOLCHAIN b/api/TOOLCHAIN new file mode 100644 index 0000000..b814f90 --- /dev/null +++ b/api/TOOLCHAIN @@ -0,0 +1,28 @@ +# このディレクトリの .api.swift を焼いたツールチェイン。 +# +# ## なぜ記録するか +# +# ベースラインは Swift コンパイラが吐く interface をそのまま記録している。 +# つまり**コンパイラのバージョンに依存する。** Xcode を上げたとき、ソースを +# 1 行も触っていないのに 3,606 行の差分が出た: +# +# - `@available` のプラットフォーム列挙順が変わっただけの行が 150 +# - `_modify` アクセサが出力されなくなった行が 50 +# - Int / String / Double / Bool の適合が明示されるようになった行が 490 +# +# 差分を見ても「API が変わった」のか「コンパイラが変わった」のか判別できない。 +# ここに書いておけば、CI が違う Xcode を引いたときに 3,000 行の差分ではなく +# 「ツールチェインが違う」と言って落ちる。 +# +# ## 上げるとき +# +# 1. ローカルの Xcode を上げる +# 2. `./scripts/api-surface.sh dump ios-sdk-core` +# 3. このファイルを更新する +# 4. CI のワークフロー(api-surface.yml / test.yml)の XCODE_VERSION も合わせる +# +# 差分は「コンパイラが変わった分」として 1 コミットに分け、API の変更と混ぜないこと。 + +XCODE_VERSION=27.0 +XCODE_BUILD=27A266a +SWIFT_VERSION=6.4