diff --git a/Sources/MapConductorCore/circle/CircleController.swift b/Sources/MapConductorCore/circle/CircleController.swift index 81636cd..c9c7ef6 100644 --- a/Sources/MapConductorCore/circle/CircleController.swift +++ b/Sources/MapConductorCore/circle/CircleController.swift @@ -119,7 +119,10 @@ where Renderer.ActualCircle == ActualCircle { await semaphore.withPermit { let entities = circleManager.allEntities() await renderer.onRemove(data: entities) + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 circleManager.clear() + await renderer.onPostProcess() } } diff --git a/Sources/MapConductorCore/groundimage/GroundImageController.swift b/Sources/MapConductorCore/groundimage/GroundImageController.swift index 1a9fcff..d1dcf7b 100644 --- a/Sources/MapConductorCore/groundimage/GroundImageController.swift +++ b/Sources/MapConductorCore/groundimage/GroundImageController.swift @@ -118,9 +118,10 @@ where Renderer.ActualGroundImage == ActualGroundImage { await semaphore.withPermit { let entities = groundImageManager.allEntities() await renderer.onRemove(data: entities) - // android-sdk / react-sdk と同じく clear() でも onPostProcess を呼ぶ。 - await renderer.onPostProcess() + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 groundImageManager.clear() + await renderer.onPostProcess() } } diff --git a/Sources/MapConductorCore/marker/AbstractMarkerController.swift b/Sources/MapConductorCore/marker/AbstractMarkerController.swift index f1714b2..285ad15 100644 --- a/Sources/MapConductorCore/marker/AbstractMarkerController.swift +++ b/Sources/MapConductorCore/marker/AbstractMarkerController.swift @@ -349,6 +349,9 @@ open class AbstractMarkerController< await renderer.onRemove(data: entities) markerManager.clear() draggingMarkerIds.removeAll() + // 他の 5 種類と同じく、manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は + // onRemove では地図に触らず、onPostProcess で manager の中身から描き直す。 + await renderer.onPostProcess() } } diff --git a/Sources/MapConductorCore/polygon/PolygonController.swift b/Sources/MapConductorCore/polygon/PolygonController.swift index 4a795a1..b7fb270 100644 --- a/Sources/MapConductorCore/polygon/PolygonController.swift +++ b/Sources/MapConductorCore/polygon/PolygonController.swift @@ -122,7 +122,10 @@ where Renderer.ActualPolygon == ActualPolygon { await semaphore.withPermit { let entities = polygonManager.allEntities() await renderer.onRemove(data: entities) + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 polygonManager.clear() + await renderer.onPostProcess() } } diff --git a/Sources/MapConductorCore/polyline/PolylineController.swift b/Sources/MapConductorCore/polyline/PolylineController.swift index 00e21f6..8ee5bb5 100644 --- a/Sources/MapConductorCore/polyline/PolylineController.swift +++ b/Sources/MapConductorCore/polyline/PolylineController.swift @@ -127,9 +127,10 @@ where Renderer.ActualPolyline == ActualPolyline { let entities = polylineManager.allEntities() await renderer.onRemove(data: entities) if polylineManager.isDestroyed { return } - await renderer.onPostProcess() - if polylineManager.isDestroyed { return } + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 polylineManager.clear() + await renderer.onPostProcess() } } diff --git a/Sources/MapConductorCore/raster/RasterLayerController.swift b/Sources/MapConductorCore/raster/RasterLayerController.swift index c5adda8..33e3e12 100644 --- a/Sources/MapConductorCore/raster/RasterLayerController.swift +++ b/Sources/MapConductorCore/raster/RasterLayerController.swift @@ -163,8 +163,10 @@ where Renderer.ActualLayer == ActualLayer { upsertedIds.removeAll() let entities = rasterLayerManager.allEntities() await renderer.onRemove(data: entities) - await renderer.onPostProcess() + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 rasterLayerManager.clear() + await renderer.onPostProcess() } } diff --git a/Tests/MapConductorCoreTests/OverlayClearOrderTests.swift b/Tests/MapConductorCoreTests/OverlayClearOrderTests.swift new file mode 100644 index 0000000..172339d --- /dev/null +++ b/Tests/MapConductorCoreTests/OverlayClearOrderTests.swift @@ -0,0 +1,194 @@ +import UIKit +import XCTest + +@testable import MapConductorCore + +/// clear() の後、地図に何も残らないこと。 +/// +/// MapLibre / Mapbox / Mappls / MapTiler などの GL 系の描画担当は、remove では何もせず、 +/// onPostProcess で manager の全部を描き直す。ここの偽レンダラも同じ作りにしてあり、 +/// `drawn` は最後の onPostProcess で描いたもの(=地図に見えているもの)を表す。 +/// +/// clear() が manager を空にする前に onPostProcess を呼ぶと、全部を描き直して残る。 +/// 呼ばなければ、前に描いたものが残る。android-sdk の OverlayClearOrderTest と同じ。 +final class OverlayClearOrderTests: XCTestCase { + private let a = GeoPoint(latitude: 35.0, longitude: 139.0) + private let b = GeoPoint(latitude: 35.1, longitude: 139.1) + private let c = GeoPoint(latitude: 35.0, longitude: 139.1) + + // MARK: - Polygon + + private final class PolygonGL: PolygonOverlayRendererProtocol { + let manager: PolygonManager + var drawn: [String] = [] + init(_ manager: PolygonManager) { self.manager = manager } + func onAdd(data: [PolygonOverlayAddParams]) async -> [String?] { data.map { $0.state.id } } + func onChange(data: [PolygonOverlayChangeParams]) async -> [String?] { data.map { $0.current.state.id } } + func onRemove(data: [PolygonEntity]) async {} + func onPostProcess() async { drawn = manager.allEntities().map { $0.state.id }.sorted() } + } + + func testPolygonClearLeavesNothingDrawn() async { + let manager = PolygonManager() + let renderer = PolygonGL(manager) + let controller = PolygonController(polygonManager: manager, renderer: renderer) + await controller.add(data: [ + PolygonState(points: [a, b, c], id: "p1"), + PolygonState(points: [a, b, c], id: "p2"), + ]) + XCTAssertEqual(renderer.drawn, ["p1", "p2"]) + + await controller.clear() + + XCTAssertEqual(renderer.drawn, []) + } + + // MARK: - Polyline + + private final class PolylineGL: PolylineOverlayRendererProtocol { + let manager: PolylineManager + var drawn: [String] = [] + init(_ manager: PolylineManager) { self.manager = manager } + func onAdd(data: [PolylineOverlayAddParams]) async -> [String?] { data.map { $0.state.id } } + func onChange(data: [PolylineOverlayChangeParams]) async -> [String?] { data.map { $0.current.state.id } } + func onRemove(data: [PolylineEntity]) async {} + func onPostProcess() async { drawn = manager.allEntities().map { $0.state.id }.sorted() } + } + + func testPolylineClearLeavesNothingDrawn() async { + let manager = PolylineManager() + let renderer = PolylineGL(manager) + let controller = PolylineController(polylineManager: manager, renderer: renderer) + await controller.add(data: [ + PolylineState(points: [a, b], id: "l1"), + PolylineState(points: [b, c], id: "l2"), + ]) + XCTAssertEqual(renderer.drawn, ["l1", "l2"]) + + await controller.clear() + + XCTAssertEqual(renderer.drawn, []) + } + + // MARK: - Circle + + private final class CircleGL: CircleOverlayRendererProtocol { + let manager: CircleManager + var drawn: [String] = [] + init(_ manager: CircleManager) { self.manager = manager } + func onAdd(data: [CircleOverlayAddParams]) async -> [String?] { data.map { $0.state.id } } + func onChange(data: [CircleOverlayChangeParams]) async -> [String?] { data.map { $0.current.state.id } } + func onRemove(data: [CircleEntity]) async {} + func onPostProcess() async { drawn = manager.allEntities().map { $0.state.id }.sorted() } + } + + func testCircleClearLeavesNothingDrawn() async { + let manager = CircleManager() + let renderer = CircleGL(manager) + let controller = CircleController(circleManager: manager, renderer: renderer) + await controller.add(data: [ + CircleState(center: a, radiusMeters: 100, id: "c1"), + CircleState(center: b, radiusMeters: 50, id: "c2"), + ]) + XCTAssertEqual(renderer.drawn, ["c1", "c2"]) + + await controller.clear() + + XCTAssertEqual(renderer.drawn, []) + } + + // MARK: - GroundImage + + private final class GroundImageGL: GroundImageOverlayRendererProtocol { + let manager: GroundImageManager + var drawn: [String] = [] + init(_ manager: GroundImageManager) { self.manager = manager } + func onAdd(data: [GroundImageOverlayAddParams]) async -> [String?] { data.map { $0.state.id } } + func onChange(data: [GroundImageOverlayChangeParams]) async -> [String?] { data.map { $0.current.state.id } } + func onRemove(data: [GroundImageEntity]) async {} + func onPostProcess() async { drawn = manager.allEntities().map { $0.state.id }.sorted() } + } + + func testGroundImageClearLeavesNothingDrawn() async { + let manager = GroundImageManager() + let renderer = GroundImageGL(manager) + let controller = GroundImageController(groundImageManager: manager, renderer: renderer) + let bounds = GeoRectBounds(southWest: a, northEast: b) + await controller.add(data: [ + GroundImageState(bounds: bounds, image: UIImage(), id: "g1"), + GroundImageState(bounds: bounds, image: UIImage(), id: "g2"), + ]) + XCTAssertEqual(renderer.drawn, ["g1", "g2"]) + + await controller.clear() + + XCTAssertEqual(renderer.drawn, []) + } + + // MARK: - RasterLayer + + private final class RasterGL: RasterLayerOverlayRendererProtocol { + let manager: RasterLayerManager + var drawn: [String] = [] + init(_ manager: RasterLayerManager) { self.manager = manager } + func onAdd(data: [RasterLayerOverlayAddParams]) async -> [String?] { data.map { $0.state.id } } + func onChange(data: [RasterLayerOverlayChangeParams]) async -> [String?] { data.map { $0.current.state.id } } + func onRemove(data: [RasterLayerEntity]) async {} + func onPostProcess() async { drawn = manager.allEntities().map { $0.state.id }.sorted() } + } + + func testRasterLayerClearLeavesNothingDrawn() async { + let manager = RasterLayerManager() + let renderer = RasterGL(manager) + let controller = RasterLayerController(rasterLayerManager: manager, renderer: renderer) + await controller.add(data: [ + RasterLayerState(source: .urlTemplate(template: "https://example.com/a/{z}/{x}/{y}.png"), id: "r1"), + RasterLayerState(source: .urlTemplate(template: "https://example.com/b/{z}/{x}/{y}.png"), id: "r2"), + ]) + XCTAssertEqual(renderer.drawn, ["r1", "r2"]) + + await controller.clear() + + XCTAssertEqual(renderer.drawn, []) + } + + // MARK: - Marker + + /// MapLibre / Mapbox / Mappls / MapTiler のマーカーの描画担当と同じ作り: onRemove は + /// アニメーションやアイコンの記録を捨てるだけで、地図のソースは onPostProcess が描き直す。 + @MainActor + private final class MarkerGL: MarkerOverlayRendererProtocol { + typealias ActualMarker = String + let manager: MarkerManager + var drawn: [String] = [] + var animateStartListener: OnMarkerEventHandler? + var animateEndListener: OnMarkerEventHandler? + init(_ manager: MarkerManager) { self.manager = manager } + func onAdd(data: [MarkerOverlayAddParams]) async -> [String?] { data.map { $0.state.id } } + func onChange(data: [MarkerOverlayChangeParams]) async -> [String?] { data.map { $0.current.state.id } } + func onRemove(data: [MarkerEntity]) async {} + func onAnimate(entity: MarkerEntity) async {} + func onPostProcess() async { drawn = manager.allEntities().map { $0.state.id }.sorted() } + } + + /// `find(position:)` が `fatalError` なので具象サブクラスを用意する。 + private final class MarkerControllerGL: AbstractMarkerController { + override func find(position: GeoPointProtocol) -> MarkerEntity? { nil } + } + + @MainActor + func testMarkerClearLeavesNothingDrawn() async { + let manager = MarkerManager() + let renderer = MarkerGL(manager) + let controller = MarkerControllerGL(markerManager: manager, renderer: renderer) + await controller.add(data: [ + MarkerState(position: a, id: "m1"), + MarkerState(position: b, id: "m2"), + ]) + XCTAssertEqual(renderer.drawn, ["m1", "m2"]) + + await controller.clear() + + XCTAssertEqual(renderer.drawn, []) + } +}