diff --git a/Sources/MapConductorCore/geocell/HexCellRegistry.swift b/Sources/MapConductorCore/geocell/HexCellRegistry.swift deleted file mode 100644 index bd67d63..0000000 --- a/Sources/MapConductorCore/geocell/HexCellRegistry.swift +++ /dev/null @@ -1,226 +0,0 @@ -import CoreGraphics -import Foundation - -/// Thread-safe registry of hex cells with a lazy KD-tree spatial index. -public final class HexCellRegistry { - private let geocell: HexGeocellProtocol - private let zoom: Double - - private var kdTree: KDTree? - private var allCells: [String: HexCell] = [:] - private var entryIDsByCell: [String: Set] = [:] - private var allEntries: [String: String] = [:] // entityId -> cellId - private var needsRebuild = false - - private let lock = NSLock() - - public init(geocell: HexGeocellProtocol, zoom: Double) { - self.geocell = geocell - self.zoom = zoom - } - - /// Get the hex cell for an entity without registering it. - public func getCell(entity: MarkerEntity) -> HexCell { - let coord = geocell.latLngToHexCoord(position: entity.state.position, zoom: zoom) - let centerLatLng = geocell.hexToLatLngCenter(coord: coord, latHint: entity.state.position.latitude, zoom: zoom) - let centerXY = geocell.projection.project(centerLatLng) - let cellId = geocell.hexToCellId(coord: coord, zoom: zoom) - return HexCell(coord: coord, centerLatLng: centerLatLng, centerXY: centerXY, id: cellId) - } - - /// Register or update an entity in the registry. - @discardableResult - public func setPoint(entity: MarkerEntity) -> HexCell { - lock.lock() - defer { lock.unlock() } - - let entityId = entity.state.id - - if let oldCellId = allEntries[entityId] { - _ = removeFromCell(cellId: oldCellId, entityId: entityId) - } - - let cell = getCell(entity: entity) - let cellId = cell.id - - allCells[cellId] = cell - allEntries[entityId] = cellId - - var entryIds = entryIDsByCell[cellId] ?? Set() - entryIds.insert(entityId) - entryIDsByCell[cellId] = entryIds - - markDirty() - return cell - } - - public func contains(hexId: String) -> Bool { - lock.lock() - defer { lock.unlock() } - return allCells[hexId] != nil - } - - /// Remove an entity from the registry. - @discardableResult - public func removePoint(entity: MarkerEntity) -> Bool { - lock.lock() - defer { lock.unlock() } - - let entityId = entity.state.id - guard let cellId = allEntries[entityId] else { return false } - - let removed = removeFromCell(cellId: cellId, entityId: entityId) - if removed { - allEntries.removeValue(forKey: entityId) - markDirty() - } - return removed - } - - private func removeFromCell(cellId: String, entityId: String) -> Bool { - guard var entryIds = entryIDsByCell[cellId] else { return false } - let removed = entryIds.remove(entityId) != nil - - if removed { - if entryIds.isEmpty { - allCells.removeValue(forKey: cellId) - entryIDsByCell.removeValue(forKey: cellId) - } else { - entryIDsByCell[cellId] = entryIds - } - } - return removed - } - - public func clear() { - lock.lock() - defer { lock.unlock() } - allCells.removeAll() - entryIDsByCell.removeAll() - allEntries.removeAll() - kdTree = nil - needsRebuild = false - } - - private func markDirty() { - needsRebuild = true - } - - private func rebuildIfNeeded() { - if !needsRebuild { return } - kdTree = allCells.isEmpty ? nil : KDTree(points: Array(allCells.values)) - needsRebuild = false - } - - public func findNearest(point: GeoPointProtocol) -> HexCell? { - lock.lock() - defer { lock.unlock() } - rebuildIfNeeded() - return kdTree?.nearest(query: geocell.projection.project(point)) - } - - public func findNearestWithDistance(point: GeoPointProtocol) -> HexCellWithDistance? { - lock.lock() - defer { lock.unlock() } - rebuildIfNeeded() - return kdTree?.nearestWithDistance(query: geocell.projection.project(point)) - } - - public func findNearestKWithDistance(point: GeoPointProtocol, k: Int) -> [HexCellWithDistance] { - lock.lock() - defer { lock.unlock() } - rebuildIfNeeded() - return kdTree?.nearestKWithDistance(query: geocell.projection.project(point), k: k) ?? [] - } - - public func findWithinRadiusWithDistance(point: GeoPointProtocol, radius: Double) -> [HexCellWithDistance] { - lock.lock() - defer { lock.unlock() } - rebuildIfNeeded() - 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() } - return Array(allCells.values) - } - - public func getEntryIDsByHexCell(_ hexCell: HexCell) -> Set? { - lock.lock() - defer { lock.unlock() } - return entryIDsByCell[hexCell.id] - } - - /// Calculates meters corresponding to `pixels` at `position` / `zoom`. - /// - /// - Note: This assumes the projection uses meters (WebMercator EPSG:3857). - public func metersPerPixel( - position: GeoPointProtocol, - zoom: Double, - pixels: Double, - tileSize: Int = 256 - ) -> Double { - precondition(pixels > 0, "Pixels must be positive") - precondition(tileSize > 0, "Tile size must be positive") - - let deltaLng = 360.0 * pixels / (Double(tileSize) * pow(2.0, zoom)) - - var newLng = position.longitude + deltaLng - if newLng > 180.0 { newLng -= 360.0 } - if newLng < -180.0 { newLng += 360.0 } - - let p1 = geocell.projection.project(position) - let p2 = geocell.projection.project(GeoPoint(latitude: position.latitude, longitude: newLng, altitude: position.altitude ?? 0.0)) - - let dx = p2.x - p1.x - let dy = p2.y - p1.y - return sqrt(dx * dx + dy * dy) - } - - public func findWithinPixelRadius( - position: GeoPointProtocol, - zoom: Double, - pixels: Double, - tileSize: Int = 256 - ) -> [HexCellWithDistance] { - let meters = metersPerPixel(position: position, zoom: zoom, pixels: pixels, tileSize: tileSize) - return findWithinRadiusWithDistance(point: position, radius: meters) - } - - public func findByIdPrefix(_ prefix: String) -> [HexCell] { - precondition(!prefix.isEmpty, "Prefix cannot be empty") - lock.lock() - defer { lock.unlock() } - return allCells - .filter { $0.key.hasPrefix(prefix) } - .map(\.value) - } - - public func getStats() -> RegistryStats { - lock.lock() - defer { lock.unlock() } - return RegistryStats( - totalCells: allCells.count, - totalEntries: allEntries.count, - kdTreeBuilt: kdTree != nil, - needsRebuild: needsRebuild - ) - } -} - -public struct RegistryStats: Sendable, Hashable { - public let totalCells: Int - public let totalEntries: Int - public let kdTreeBuilt: Bool - public let needsRebuild: Bool -} - diff --git a/Sources/MapConductorCore/geocell/HexGeocell.swift b/Sources/MapConductorCore/geocell/HexGeocell.swift index 8ec9358..50bb15e 100644 --- a/Sources/MapConductorCore/geocell/HexGeocell.swift +++ b/Sources/MapConductorCore/geocell/HexGeocell.swift @@ -73,11 +73,6 @@ public struct HexCell: Sendable, Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(id) } } -public struct HexCellWithDistance: Sendable, Hashable { - public let cell: HexCell - public let distanceMeters: Double -} - public struct IdentifiedHexCell: Sendable, Hashable { public let id: String public let cell: HexCell diff --git a/Sources/MapConductorCore/geocell/KDTree.swift b/Sources/MapConductorCore/geocell/KDTree.swift deleted file mode 100644 index 00f221a..0000000 --- a/Sources/MapConductorCore/geocell/KDTree.swift +++ /dev/null @@ -1,285 +0,0 @@ -import CoreGraphics -import Foundation - -/// K-D tree for spatial queries on `HexCell.centerXY` (projected meters). -public final class KDTree: Sendable { - private final class Node: @unchecked Sendable { - let cell: HexCell - let left: Node? - let right: Node? - let axis: Int // 0 = x, 1 = y - - init(cell: HexCell, left: Node?, right: Node?, axis: Int) { - self.cell = cell - self.left = left - self.right = right - self.axis = axis - } - } - - private let root: Node? - - public init(points: [HexCell]) { - self.root = points.isEmpty ? nil : Self.build(items: points, depth: 0) - } - - private static func build(items: [HexCell], depth: Int) -> Node? { - if items.isEmpty { return nil } - - let axis = depth % 2 - let sorted = items.sorted { lhs, rhs in - if axis == 0 { lhs.centerXY.x < rhs.centerXY.x } - else { lhs.centerXY.y < rhs.centerXY.y } - } - let mid = sorted.count / 2 - - return Node( - cell: sorted[mid], - left: build(items: Array(sorted[.. HexCell? { - guard let root else { return nil } - return nearest(node: root, query: query, best: nil, bestDistSq: .greatestFiniteMagnitude) - } - - private func nearest(node: Node, query: CGPoint, best: HexCell?, bestDistSq: Double) -> HexCell? { - 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 distSq = squaredDistance(query, node.cell.centerXY) - var currentBest = best - var currentBestDistSq = bestDistSq - - if distSq < currentBestDistSq { - currentBest = node.cell - currentBestDistSq = distSq - } - - let nearChild: Node? - let farChild: Node? - if queryVal < nodeVal { - nearChild = node.left - farChild = node.right - } else { - nearChild = node.right - farChild = node.left - } - - if let nearChild { - currentBest = nearest(node: nearChild, query: query, best: currentBest, bestDistSq: currentBestDistSq) - if let currentBest { - currentBestDistSq = squaredDistance(query, currentBest.centerXY) - } - } - - if let farChild { - let axisDist = (queryVal - nodeVal) * (queryVal - nodeVal) - if axisDist < currentBestDistSq { - currentBest = nearest(node: farChild, query: query, best: currentBest, bestDistSq: currentBestDistSq) - } - } - - return currentBest - } - - public func nearestWithDistance(query: CGPoint) -> HexCellWithDistance? { - guard let cell = nearest(query: query) else { return nil } - return HexCellWithDistance(cell: cell, distanceMeters: sqrt(squaredDistance(query, cell.centerXY))) - } - - public func nearestKWithDistance(query: CGPoint, k: Int) -> [HexCellWithDistance] { - precondition(k > 0, "k must be positive") - guard let root else { return [] } - - var heap = MaxCellHeap(capacity: k) - nearestK(node: root, query: query, k: k, heap: &heap) - - return heap - .elements - .map { HexCellWithDistance(cell: $0.cell, distanceMeters: sqrt($0.distSq)) } - .sorted { $0.distanceMeters < $1.distanceMeters } - } - - private func nearestK(node: Node, query: CGPoint, k: Int, heap: inout MaxCellHeap) { - let distSq = squaredDistance(query, node.cell.centerXY) - heap.push(distSq: distSq, cell: 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: Node? - let farChild: Node? - if queryVal < nodeVal { - nearChild = node.left - farChild = node.right - } else { - nearChild = node.right - farChild = node.left - } - - if let nearChild { nearestK(node: nearChild, query: query, k: k, heap: &heap) } - - if let farChild { - let axisDist = (queryVal - nodeVal) * (queryVal - nodeVal) - if heap.elements.count < k || axisDist < heap.maxDistSq { - nearestK(node: farChild, query: query, k: k, heap: &heap) - } - } - } - - public func withinRadiusWithDistance(query: CGPoint, radius: Double) -> [HexCellWithDistance] { - precondition(radius >= 0, "Radius must be non-negative") - guard let root else { return [] } - - var results: [HexCellWithDistance] = [] - withinRadius(node: root, query: query, radiusSq: radius * radius, results: &results) - results.sort { $0.distanceMeters < $1.distanceMeters } - 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 { - results.append(HexCellWithDistance(cell: node.cell, distanceMeters: sqrt(distSq))) - } - - 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: Node? - let farChild: Node? - if queryVal < nodeVal { - nearChild = node.left - farChild = node.right - } else { - nearChild = node.right - farChild = node.left - } - - if let nearChild { withinRadius(node: nearChild, query: query, radiusSq: radiusSq, results: &results) } - - if let farChild { - let axisDist = (queryVal - nodeVal) * (queryVal - nodeVal) - if axisDist <= radiusSq { - withinRadius(node: farChild, query: query, radiusSq: radiusSq, results: &results) - } - } - } - - private func squaredDistance(_ a: CGPoint, _ b: CGPoint) -> Double { - let dx = a.x - b.x - let dy = a.y - b.y - return dx * dx + dy * dy - } -} - -/// Fixed-size max heap of (distance^2, cell) for k-NN. -private struct MaxCellHeap { - struct Entry { - let distSq: Double - let cell: HexCell - } - - private(set) var elements: [Entry] = [] - private let capacity: Int - - init(capacity: Int) { - self.capacity = max(0, capacity) - self.elements.reserveCapacity(capacity) - } - - var maxDistSq: Double { elements.first?.distSq ?? .greatestFiniteMagnitude } - - mutating func push(distSq: Double, cell: HexCell) { - guard capacity > 0 else { return } - - if elements.count < capacity { - elements.append(Entry(distSq: distSq, cell: cell)) - siftUp(from: elements.count - 1) - return - } - - if let max = elements.first, distSq >= max.distSq { return } - - elements[0] = Entry(distSq: distSq, cell: cell) - siftDown(from: 0) - } - - private mutating func siftUp(from index: Int) { - var child = index - while child > 0 { - let parent = (child - 1) / 2 - if elements[child].distSq <= elements[parent].distSq { break } - elements.swapAt(child, parent) - child = parent - } - } - - private mutating func siftDown(from index: Int) { - var parent = index - while true { - let left = parent * 2 + 1 - let right = left + 1 - var candidate = parent - - if left < elements.count, elements[left].distSq > elements[candidate].distSq { - candidate = left - } - if right < elements.count, elements[right].distSq > elements[candidate].distSq { - candidate = right - } - if candidate == parent { break } - - elements.swapAt(parent, candidate) - parent = candidate - } - } -} diff --git a/Sources/MapConductorCore/marker/MarkerGridIndex.swift b/Sources/MapConductorCore/marker/MarkerGridIndex.swift index f88dd0d..b381f14 100644 --- a/Sources/MapConductorCore/marker/MarkerGridIndex.swift +++ b/Sources/MapConductorCore/marker/MarkerGridIndex.swift @@ -2,11 +2,11 @@ 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 +/// Replaced the hex-cell registry, since removed, 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 diff --git a/Sources/MapConductorCore/marker/MarkerManager.swift b/Sources/MapConductorCore/marker/MarkerManager.swift index 02ee9cb..f43c014 100644 --- a/Sources/MapConductorCore/marker/MarkerManager.swift +++ b/Sources/MapConductorCore/marker/MarkerManager.swift @@ -11,9 +11,6 @@ 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 @@ -72,7 +69,6 @@ public final class MarkerManager { guard usableLocked("removeEntity") else { return nil } let removed = entities.removeValue(forKey: id) if let removed { - cellRegistry?.removePoint(entity: removed) gridIndex.invalidate() } return removed @@ -83,7 +79,6 @@ public final class MarkerManager { defer { lock.unlock() } guard usableLocked("registerEntity") else { return } entities[entity.state.id] = entity - cellRegistry?.setPoint(entity: entity) gridIndex.invalidate() } @@ -92,7 +87,6 @@ public final class MarkerManager { defer { lock.unlock() } guard usableLocked("updateEntity") else { return } entities[entity.state.id] = entity - cellRegistry?.setPoint(entity: entity) gridIndex.invalidate() } @@ -132,26 +126,6 @@ public final class MarkerManager { } } - public func findByIdPrefix(_ prefix: String) -> [HexCell] { - lock.lock() - defer { lock.unlock() } - guard usableLocked("findByIdPrefix") else { return [] } - // 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 { - if let cellRegistry { return cellRegistry } - - let newRegistry = HexCellRegistry(geocell: geocell, zoom: 20.0) - for entity in entities.values { - newRegistry.setPoint(entity: entity) - } - cellRegistry = newRegistry - return newRegistry - } - public func allEntities() -> [MarkerEntity] { lock.lock() defer { lock.unlock() } @@ -218,11 +192,7 @@ public final class MarkerManager { private func estimateMemoryUsageLocked() -> Int64 { let entityMapOverhead = Int64(entities.count) * 64 let entityObjects = Int64(entities.count) * 200 - 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 + let spatialIndexSize = gridIndex.estimatedBytes() return entityMapOverhead + entityObjects + spatialIndexSize } @@ -231,7 +201,6 @@ public final class MarkerManager { defer { lock.unlock() } guard usableLocked("clear") else { return } entities.removeAll() - cellRegistry?.clear() gridIndex.invalidate() } @@ -241,8 +210,6 @@ public final class MarkerManager { if destroyed { return } destroyed = true entities.removeAll() - cellRegistry?.clear() - cellRegistry = nil gridIndex.invalidate() } diff --git a/Tests/MapConductorCoreTests/KDTreeRadiusTests.swift b/Tests/MapConductorCoreTests/KDTreeRadiusTests.swift deleted file mode 100644 index bc31d9e..0000000 --- a/Tests/MapConductorCoreTests/KDTreeRadiusTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -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/MarkerManagerDestroyTests.swift b/Tests/MapConductorCoreTests/MarkerManagerDestroyTests.swift index 93cb023..1ba9057 100644 --- a/Tests/MapConductorCoreTests/MarkerManagerDestroyTests.swift +++ b/Tests/MapConductorCoreTests/MarkerManagerDestroyTests.swift @@ -57,7 +57,6 @@ final class MarkerManagerDestroyTests: XCTestCase { XCTAssertNil(manager.removeEntity("a")) XCTAssertTrue(manager.allEntities().isEmpty) XCTAssertNil(manager.findNearest(position: GeoPoint(latitude: 35.0, longitude: 139.0))) - XCTAssertTrue(manager.findByIdPrefix("a").isEmpty) } /// destroy は何度呼んでもよい。 diff --git a/api/MapConductorCore.api.swift b/api/MapConductorCore.api.swift index 7c9895f..987d247 100644 --- a/api/MapConductorCore.api.swift +++ b/api/MapConductorCore.api.swift @@ -712,39 +712,6 @@ public enum Earth { 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 - @discardableResult - 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 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 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 { - get - } -} public struct HexCoord : Swift::Sendable, Swift::Hashable, Swift::CustomStringConvertible { public let q: Swift::Int public let r: Swift::Int @@ -793,15 +760,6 @@ public struct HexCell : Swift::Sendable, Swift::Hashable { 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 @@ -839,15 +797,6 @@ public struct HexGeocell : MapConductorCore::HexGeocellProtocol { 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 @@ -1871,7 +1820,6 @@ final public class MarkerManager { 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]