From c6b11b8202724d8005a6fd4e45c75aab2d6e1cd6 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 18:47:41 -0800 Subject: [PATCH 1/2] Clear overlays in the order remove, empty the manager, then post-process GL-style renderers (MapLibre, Mapbox, Mappls, MapTiler, TomTom, Longdo) ignore onRemove and redraw everything the manager holds in onPostProcess. Polyline, GroundImage and RasterLayer called onPostProcess before emptying the manager, so clear() redrew every overlay it was meant to remove; Polygon and Circle did not call it at all. All five now run onRemove, manager.clear(), onPostProcess, matching android-sdk-core and ios-sdk-core. This is what the providers' clear() overrides (super.clear() followed by renderer.redraw()) worked around; they can go once this is published. OverlayClearOrder.test.mjs pins this with a GL-style fake renderer for each kind. --- src/circle/CircleController.ts | 3 + src/groundimage/GroundImageController.ts | 4 +- src/polygon/PolygonController.ts | 3 + src/polyline/PolylineController.ts | 4 +- src/raster/RasterLayerController.ts | 4 +- test/OverlayClearOrder.test.mjs | 118 +++++++++++++++++++++++ 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 test/OverlayClearOrder.test.mjs diff --git a/src/circle/CircleController.ts b/src/circle/CircleController.ts index 0486b6a..5715370 100644 --- a/src/circle/CircleController.ts +++ b/src/circle/CircleController.ts @@ -158,7 +158,10 @@ export abstract class CircleController async clear(): Promise { await this.semaphore.withLock(async () => { await this.renderer.onRemove(this.circleManager.allEntities()); + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 this.circleManager.clear(); + await this.renderer.onPostProcess(); }); } diff --git a/src/groundimage/GroundImageController.ts b/src/groundimage/GroundImageController.ts index b3e2a8a..92e3397 100644 --- a/src/groundimage/GroundImageController.ts +++ b/src/groundimage/GroundImageController.ts @@ -160,8 +160,10 @@ export abstract class GroundImageController async clear(): Promise { await this.semaphore.withLock(async () => { await this.renderer.onRemove(this.groundImageManager.allEntities()); - await this.renderer.onPostProcess(); + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 this.groundImageManager.clear(); + await this.renderer.onPostProcess(); }); } diff --git a/src/polygon/PolygonController.ts b/src/polygon/PolygonController.ts index 3373cf0..d68ef41 100644 --- a/src/polygon/PolygonController.ts +++ b/src/polygon/PolygonController.ts @@ -165,7 +165,10 @@ export abstract class PolygonController async clear(): Promise { await this.semaphore.withLock(async () => { await this.renderer.onRemove(this.polygonManager.allEntities()); + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 this.polygonManager.clear(); + await this.renderer.onPostProcess(); }); } diff --git a/src/polyline/PolylineController.ts b/src/polyline/PolylineController.ts index 8f3b88c..9dd301c 100644 --- a/src/polyline/PolylineController.ts +++ b/src/polyline/PolylineController.ts @@ -161,8 +161,10 @@ export abstract class PolylineController async clear(): Promise { await this.semaphore.withLock(async () => { await this.renderer.onRemove(this.polylineManager.allEntities()); - await this.renderer.onPostProcess(); + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 this.polylineManager.clear(); + await this.renderer.onPostProcess(); }); } diff --git a/src/raster/RasterLayerController.ts b/src/raster/RasterLayerController.ts index b653622..b08bc90 100644 --- a/src/raster/RasterLayerController.ts +++ b/src/raster/RasterLayerController.ts @@ -240,8 +240,10 @@ export abstract class RasterLayerController await this.semaphore.withLock(async () => { this.upsertedIds.clear(); await this.renderer.onRemove(this.rasterLayerManager.allEntities()); - await this.renderer.onPostProcess(); + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 this.rasterLayerManager.clear(); + await this.renderer.onPostProcess(); }); this.syncHeaders(); } diff --git a/test/OverlayClearOrder.test.mjs b/test/OverlayClearOrder.test.mjs new file mode 100644 index 0000000..94f6749 --- /dev/null +++ b/test/OverlayClearOrder.test.mjs @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + CircleController, + CircleManager, + GroundImageController, + GroundImageManager, + PolygonController, + PolygonManager, + PolylineController, + PolylineManager, + RasterLayerController, + RasterLayerManager, + RasterLayerSource, + createCircleState, + createGeoPoint, + createGeoRectBounds, + createGroundImageState, + createPolygonState, + createPolylineState, + createRasterLayerState, +} from '../dist/index.mjs'; + +/** + * clear() の後、地図に何も残らないこと。 + * + * MapLibre / Mapbox / Mappls / MapTiler / TomTom / Longdo などの GL 系の描画担当は、 + * remove では何もせず、onPostProcess で manager の全部を描き直す。ここの偽レンダラも + * 同じ作りにしてあり、`drawn` は最後の onPostProcess で描いたもの(=地図に見えているもの)を表す。 + * + * clear() が manager を空にする前に onPostProcess を呼ぶと、全部を描き直して残る。 + * 呼ばなければ、前に描いたものが残る。android-sdk / ios-sdk の OverlayClearOrder テストと同じ。 + */ + +const a = createGeoPoint({ latitude: 35.0, longitude: 139.0 }); +const b = createGeoPoint({ latitude: 35.1, longitude: 139.1 }); +const c = createGeoPoint({ latitude: 35.0, longitude: 139.1 }); + +/** GL 系の描画担当の作り。 */ +class GlRenderer { + constructor(manager) { + this.manager = manager; + this.drawn = []; + } + async onAdd(data) { return data.map((p) => p.state.id); } + async onChange(data) { return data.map((p) => p.current.state.id); } + async onRemove(_data) {} + async onCameraChanged(_camera) {} + async onPostProcess() { this.drawn = this.manager.allEntities().map((e) => e.state.id).sort(); } +} + +const kinds = [ + { + name: 'polygon', + make: (manager, renderer) => new (class extends PolygonController {})({ polygonManager: manager, renderer }), + manager: () => new PolygonManager(), + states: () => [ + createPolygonState({ points: [a, b, c], id: 'p1' }), + createPolygonState({ points: [a, b, c], id: 'p2' }), + ], + }, + { + name: 'polyline', + make: (manager, renderer) => new (class extends PolylineController {})({ polylineManager: manager, renderer }), + manager: () => new PolylineManager(), + states: () => [ + createPolylineState({ points: [a, b], id: 'p1' }), + createPolylineState({ points: [b, c], id: 'p2' }), + ], + }, + { + name: 'circle', + make: (manager, renderer) => new (class extends CircleController {})({ circleManager: manager, renderer }), + manager: () => new CircleManager(), + states: () => [ + createCircleState({ center: a, radiusMeters: 100, id: 'p1' }), + createCircleState({ center: b, radiusMeters: 50, id: 'p2' }), + ], + }, + { + name: 'groundImage', + make: (manager, renderer) => + new (class extends GroundImageController {})({ groundImageManager: manager, renderer }), + manager: () => new GroundImageManager(), + states: () => { + const bounds = createGeoRectBounds({ southWest: a, northEast: b }); + return [ + createGroundImageState({ bounds, imageUrl: 'https://example.com/a.png', id: 'p1' }), + createGroundImageState({ bounds, imageUrl: 'https://example.com/b.png', id: 'p2' }), + ]; + }, + }, + { + name: 'rasterLayer', + make: (manager, renderer) => + new (class extends RasterLayerController {})({ rasterLayerManager: manager, renderer }), + manager: () => new RasterLayerManager(), + states: () => [ + createRasterLayerState({ source: RasterLayerSource.UrlTemplate({ template: 'https://example.com/a/{z}/{x}/{y}.png' }), id: 'p1' }), + createRasterLayerState({ source: RasterLayerSource.UrlTemplate({ template: 'https://example.com/b/{z}/{x}/{y}.png' }), id: 'p2' }), + ], + }, +]; + +for (const kind of kinds) { + test(`${kind.name} の clear 後は何も描かれていない`, async () => { + const manager = kind.manager(); + const renderer = new GlRenderer(manager); + const controller = kind.make(manager, renderer); + await controller.add(kind.states()); + assert.deepEqual(renderer.drawn, ['p1', 'p2']); + + await controller.clear(); + + assert.deepEqual(renderer.drawn, []); + }); +} From a990266d52483a397aeb7c6c1c5c3563bdeaba35 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 20:04:31 -0800 Subject: [PATCH 2/2] Post-process after clearing markers too AbstractMarkerController.clear() emptied the manager without calling onPostProcess, so a GL-style renderer that only redraws its source there kept showing the markers. The MapLibre-family providers worked around it by calling onPostProcess after super.clear(); clear now ends the same way as the other five overlay kinds, and as android-sdk-core / ios-sdk-core. OverlayClearOrder.test.mjs gains the marker case. --- src/controller/AbstractMarkerController.ts | 3 +++ test/OverlayClearOrder.test.mjs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/controller/AbstractMarkerController.ts b/src/controller/AbstractMarkerController.ts index c25fbea..7f81923 100644 --- a/src/controller/AbstractMarkerController.ts +++ b/src/controller/AbstractMarkerController.ts @@ -327,6 +327,9 @@ export abstract class AbstractMarkerController await this.renderer.onRemove(rendered); } this.markerManager.clear(); + // 他の 5 種類と同じく、manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は + // onRemove では地図に触らず、onPostProcess で manager の中身から描き直す。 + await this.renderer.onPostProcess(); }); } diff --git a/test/OverlayClearOrder.test.mjs b/test/OverlayClearOrder.test.mjs index 94f6749..c4be813 100644 --- a/test/OverlayClearOrder.test.mjs +++ b/test/OverlayClearOrder.test.mjs @@ -2,10 +2,13 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { + AbstractMarkerController, CircleController, CircleManager, GroundImageController, GroundImageManager, + HexGeocellImpl, + MarkerManager, PolygonController, PolygonManager, PolylineController, @@ -17,6 +20,7 @@ import { createGeoPoint, createGeoRectBounds, createGroundImageState, + createMarkerState, createPolygonState, createPolylineState, createRasterLayerState, @@ -46,6 +50,7 @@ class GlRenderer { async onAdd(data) { return data.map((p) => p.state.id); } async onChange(data) { return data.map((p) => p.current.state.id); } async onRemove(_data) {} + async onAnimate(_entity) {} async onCameraChanged(_camera) {} async onPostProcess() { this.drawn = this.manager.allEntities().map((e) => e.state.id).sort(); } } @@ -101,6 +106,16 @@ const kinds = [ createRasterLayerState({ source: RasterLayerSource.UrlTemplate({ template: 'https://example.com/b/{z}/{x}/{y}.png' }), id: 'p2' }), ], }, + { + // MapLibre / Mapbox などのマーカーの描画担当も onRemove では地図に触らない。 + name: 'marker', + make: (manager, renderer) => new (class extends AbstractMarkerController {})({ markerManager: manager, renderer }), + manager: () => new MarkerManager(HexGeocellImpl.defaultGeocell()), + states: () => [ + createMarkerState({ position: a, id: 'p1' }), + createMarkerState({ position: b, id: 'p2' }), + ], + }, ]; for (const kind of kinds) {