From 1d652e0625d08ecafb792ce89d9e7396642aea9a Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 4 Nov 2026 18:32:43 -0800 Subject: [PATCH] Remove the unused K-D tree and hex cell registry Since f6d85c8 moved MarkerManager's spatial queries onto MarkerGridIndex, HexCellRegistry has only been built lazily by MarkerManager.findByIdPrefix, and nothing calls findByIdPrefix: no SDK module, sample or doc, only one assertion in MarkerManagerDestroyTests. KDTree existed only to back the registry, and HexCellWithDistance and RegistryStats only to carry their results. Delete KDTree, HexCellRegistry, RegistryStats, HexCellWithDistance and MarkerManager.findByIdPrefix, along with the lazy registry plumbing in MarkerManager and its share of the memory estimate. HexGeocell, HexCell and HexGeocellProtocol stay; MarkerManager and others still take a geocell. This is a breaking change to the public API; the baseline drops exactly those declarations. The same removal is being made in android-sdk-core and js-sdk-core. --- .../geocell/HexCellRegistry.swift | 226 -------------- .../MapConductorCore/geocell/HexGeocell.swift | 5 - Sources/MapConductorCore/geocell/KDTree.swift | 285 ------------------ .../marker/MarkerGridIndex.swift | 10 +- .../marker/MarkerManager.swift | 35 +-- .../KDTreeRadiusTests.swift | 45 --- .../MarkerManagerDestroyTests.swift | 1 - api/MapConductorCore.api.swift | 52 ---- 8 files changed, 6 insertions(+), 653 deletions(-) delete mode 100644 Sources/MapConductorCore/geocell/HexCellRegistry.swift delete mode 100644 Sources/MapConductorCore/geocell/KDTree.swift delete mode 100644 Tests/MapConductorCoreTests/KDTreeRadiusTests.swift 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]