From 12dbebae8257c6fe975cb4b3399f738617e879eb Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 23:55:27 -0800 Subject: [PATCH] Put app rasters under the SDK overlays; restack native shapes by band Composed rasters were appended at the end of the style, above the SDK's polygon/polyline/marker layers, so a RasterLayerState covered polygons, polylines and ground images. App rasters are now inserted before the first label or SDK overlay layer of the composed style, in zIndex order; marker tiles stay on top. TomTom's native Polygon/Polyline have no draw order and stack in add order; ground images, polygons and circle fills are all native Polygons and circle outlines are Polylines. When something is added to a lower band, the bands above it are cleared and added again so the order follows the bands. Stacking order (all providers, all platforms), bottom to top: raster layers < ground images < polygons < circles < polylines < markers. Needs android-sdk-core with OverlayStacking (fix/overlay-stacking-order). --- api/android-for-tomtom.api | 4 ++ .../tomtom/TomTomMapViewController.kt | 68 +++++++++++++++++++ .../tomtom/TomTomMapViewControllerRaster.kt | 10 ++- .../tomtom/raster/TomTomStyleComposer.kt | 61 ++++++++++++++++- 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/api/android-for-tomtom.api b/api/android-for-tomtom.api index 940e2ea..eaa0c3a 100644 --- a/api/android-for-tomtom.api +++ b/api/android-for-tomtom.api @@ -69,6 +69,10 @@ public final class com.mapconductor.tomtom.TomTomMapViewController extends com.m public void destroy(); public java.lang.Object clearOverlays(kotlin.coroutines.Continuation); public void setMarkerAnimationOverlayHost(com.mapconductor.core.marker.MarkerAnimationOverlayHost); + public java.lang.Object compositionGroundImages(java.util.List, kotlin.coroutines.Continuation); + public java.lang.Object compositionPolygons(java.util.List, kotlin.coroutines.Continuation); + public java.lang.Object compositionCircles(java.util.List, kotlin.coroutines.Continuation); + public java.lang.Object compositionPolylines(java.util.List, kotlin.coroutines.Continuation); public boolean hasPolyline(com.mapconductor.core.polyline.PolylineState); public void setOnPolylineClickListener(kotlin.jvm.functions.Function1); public boolean hasPolygon(com.mapconductor.core.polygon.PolygonState); diff --git a/src/main/java/com/mapconductor/tomtom/TomTomMapViewController.kt b/src/main/java/com/mapconductor/tomtom/TomTomMapViewController.kt index 00232d1..a60e25b 100644 --- a/src/main/java/com/mapconductor/tomtom/TomTomMapViewController.kt +++ b/src/main/java/com/mapconductor/tomtom/TomTomMapViewController.kt @@ -224,6 +224,70 @@ class TomTomMapViewController internal constructor( (markerController.renderer as TomTomMarkerRenderer).animationOverlayHost = host } + // ---- 重なり順 ---------------------------------------------------------- + // + // TomTom のネイティブの Polygon / Polyline には重なり順の指定が無く、後から足したものが + // 上に積まれる。GroundImage・ポリゴン・サークルの塗りはどれもネイティブの Polygon、 + // サークルの輪郭とポリラインはネイティブの Polyline なので、そのままだと + // 「追加した順」で重なる(clearOverlays の後に送り直すと順が変わる)。 + // 帯の順(GroundImage < ポリゴン < サークル < ポリライン)を守るため、下の帯へ + // 新しく足されたら、それより上の帯を足し直す。帯の中は zIndex の順(同じなら渡された順)。 + + override suspend fun compositionGroundImages(data: List) { + val added = data.any { !groundImageController.groundImageManager.hasEntity(it.id) } + groundImageController.add(data) + if (added) { + restackPolygons() + restackCircles() + } + } + + override suspend fun compositionPolygons(data: List) { + val added = data.any { !polygonController.polygonManager.hasEntity(it.id) } + polygonController.add(data.sortedBy { it.zIndex }) + if (added) { + restackPolygons() + restackCircles() + } + } + + override suspend fun compositionCircles(data: List) { + val added = data.any { !circleController.circleManager.hasEntity(it.id) } + circleController.add(data.sortedBy { it.zIndex ?: 0 }) + if (added) { + restackCircles() + restackPolylines() + } + } + + override suspend fun compositionPolylines(data: List) { + val added = data.any { !polylineController.polylineManager.hasEntity(it.id) } + polylineController.add(data.sortedBy { it.zIndex }) + if (added) restackPolylines() + } + + /** いったん消して zIndex の順に足し直す(ネイティブの積み順を作り直す)。 */ + private suspend fun restackPolygons() { + val states = polygonController.polygonManager.allEntities().map { it.state } + if (states.isEmpty()) return + polygonController.clear() + polygonController.add(states.sortedBy { it.zIndex }) + } + + private suspend fun restackCircles() { + val states = circleController.circleManager.allEntities().map { it.state } + if (states.isEmpty()) return + circleController.clear() + circleController.add(states.sortedBy { it.zIndex ?: 0 }) + } + + private suspend fun restackPolylines() { + val states = polylineController.polylineManager.allEntities().map { it.state } + if (states.isEmpty()) return + polylineController.clear() + polylineController.add(states.sortedBy { it.zIndex }) + } + // ---- Polyline / Polygon / Circle capable ------------------------------ override fun hasPolyline(state: PolylineState): Boolean = polylineController.polylineManager.hasEntity(state.id) @@ -345,6 +409,9 @@ class TomTomMapViewController internal constructor( // 注意: 合成は現状 browsing/light ベース固定。Standard 以外のデザインでラスタを載せた場合は // ベースが browsing になる(対象ページは Standard のため実用上問題なし)。 internal val composedRasterLayers = LinkedHashMap() + + /** アプリのラスターの zIndex(帯の中の順)。[composedRasterLayers] と同じキー。 */ + internal val composedRasterZIndex = java.util.concurrent.ConcurrentHashMap() internal val composedStyleMutex = Mutex() internal var rasterApiKey: String? = null internal var rasterCacheDir: File? = null @@ -406,6 +473,7 @@ class TomTomMapViewController internal constructor( } override suspend fun removeRasterLayer(id: String) { + composedRasterZIndex.remove(id) val removed = composedStyleMutex.withLock { composedRasterLayers.remove(id) != null } if (!removed) return scheduleComposedStyleReload() diff --git a/src/main/java/com/mapconductor/tomtom/TomTomMapViewControllerRaster.kt b/src/main/java/com/mapconductor/tomtom/TomTomMapViewControllerRaster.kt index 46b9773..d077dc2 100644 --- a/src/main/java/com/mapconductor/tomtom/TomTomMapViewControllerRaster.kt +++ b/src/main/java/com/mapconductor/tomtom/TomTomMapViewControllerRaster.kt @@ -54,6 +54,8 @@ internal suspend fun TomTomMapViewController.applyPublicRasterLayer(state: Raste removeRasterLayer(state.id) return } + // 帯の中の順(zIndex、同じなら渡された順)。合成のときに並べる。 + composedRasterZIndex[state.id] = state.zIndex // ソースの tileSize / minZoom / maxZoom を合成スタイルへ伝える。maxZoom を渡すと // 高ズームでオーバーズーム表示され、実タイルの無い領域での歯抜けを防げる。 upsertRasterLayer( @@ -94,11 +96,15 @@ internal suspend fun TomTomMapViewController.applyComposedStyle() { } composedStyleToggle = composedStyleToggle xor 1 val outFile = File(cacheDir, "tomtom_composed_style_$composedStyleToggle.json") + // アプリのラスターはラスターの帯(SDK のオーバーレイの下)、マーカーのタイルはマーカーの帯(最前面)。 + val (markerTiles, appRasters) = + composedRasterLayers.values.partition { it.id == TomTomMapViewController.MARKER_RASTER_ID } val uri = - TomTomStyleComposer.composeRasterStyle( + TomTomStyleComposer.composeStackedRasterStyle( apiKey = apiKey, cacheDir = cacheDir, - layers = composedRasterLayers.values.toList(), + belowOverlays = appRasters.sortedBy { composedRasterZIndex[it.id] ?: 0 }, + aboveOverlays = markerTiles, outFile = outFile, withoutBasemap = withoutBasemap, ) ?: return diff --git a/src/main/java/com/mapconductor/tomtom/raster/TomTomStyleComposer.kt b/src/main/java/com/mapconductor/tomtom/raster/TomTomStyleComposer.kt index ae86578..a4fd5e4 100644 --- a/src/main/java/com/mapconductor/tomtom/raster/TomTomStyleComposer.kt +++ b/src/main/java/com/mapconductor/tomtom/raster/TomTomStyleComposer.kt @@ -84,6 +84,35 @@ object TomTomStyleComposer { outFile: File = File(cacheDir, "tomtom_composed_style.json"), // [TomTomMapDesign.None]: ベースマップを描かない(本文のコメント参照)。 withoutBasemap: Boolean = false, + ): Uri? = + composeStackedRasterStyle( + apiKey = apiKey, + cacheDir = cacheDir, + belowOverlays = emptyList(), + aboveOverlays = layers, + outFile = outFile, + withoutBasemap = withoutBasemap, + ) + + /** + * [composeRasterStyle] の、重なり順を指定できる版。 + * + * - [belowOverlays]: アプリのラスター(RasterLayerState)。重なり順の決まり + * (ラスター < GroundImage < ポリゴン < サークル < ポリライン < マーカー)でいちばん下の帯なので、 + * SDK のオーバーレイ用レイヤー(polygon-shape など)とベースマップのラベルより**下**へ、 + * 渡された順(下→上)に入れる。 + * - [aboveOverlays]: マーカーのタイル。マーカーの帯なのでスタイルの末尾(最前面)へ。 + * + * 以前はどちらも末尾に積んでいたため、アプリのラスターがポリゴン・ポリライン・ + * GroundImage(いずれも SDK のオーバーレイ)を覆っていた。 + */ + internal suspend fun composeStackedRasterStyle( + apiKey: String, + cacheDir: File, + belowOverlays: List, + aboveOverlays: List, + outFile: File, + withoutBasemap: Boolean, ): Uri? = withContext(Dispatchers.IO) { // The SDK will not take a style that has none of its own layers @@ -164,8 +193,17 @@ object TomTomStyleComposer { insertLayerAt(styleLayers, insertAt, baseLayer) } - // 自前ラスタレイヤー(マーカータイル / GroundImage など)を最前面(末尾)へ順に重ねる。 - layers.forEachIndexed { index, spec -> + // アプリのラスター: SDK のオーバーレイ用レイヤーとラベルの下へ、渡された順に。 + var insertBelow = overlayOrLabelIndex(styleLayers) + belowOverlays.forEachIndexed { index, spec -> + val sourceId = "mc-raster-src-$index" + sources.put(sourceId, rasterSource(spec.tilesUrl, spec.tileSize, spec.minZoom, spec.maxZoom)) + insertLayerAt(styleLayers, insertBelow, rasterLayer("mc-raster-layer-$index", sourceId, spec.opacity)) + insertBelow++ + } + // マーカーのタイル: 最前面(末尾)へ順に重ねる。 + aboveOverlays.forEachIndexed { i, spec -> + val index = belowOverlays.size + i val sourceId = "mc-raster-src-$index" sources.put(sourceId, rasterSource(spec.tilesUrl, spec.tileSize, spec.minZoom, spec.maxZoom)) styleLayers.put(rasterLayer("mc-raster-layer-$index", sourceId, spec.opacity)) @@ -179,7 +217,8 @@ object TomTomStyleComposer { } Log.i( TAG, - "Composed style written (${outFile.length()} bytes, ${styleLayers.length()} layers, ${layers.size} raster)", + "Composed style written (${outFile.length()} bytes, ${styleLayers.length()} layers, " + + "${belowOverlays.size + aboveOverlays.size} raster)", ) Uri.fromFile(outFile) } @@ -230,6 +269,22 @@ object TomTomStyleComposer { put("paint", JSONObject().put("raster-opacity", opacity)) } + /** + * アプリのラスターを入れる位置: ベースマップのラベル(symbol)か SDK のオーバーレイ用レイヤー + * (polygon-shape / polyline-shape / circle-shape / marker / route など)のうち最初のもの。 + * どちらも無ければ末尾。 + */ + private fun overlayOrLabelIndex(layers: JSONArray): Int { + for (i in 0 until layers.length()) { + val layer = layers.optJSONObject(i) ?: continue + val id = layer.optString("id") + if (layer.optString("type") == "symbol" || SDK_OVERLAY_LAYER_HINTS.any { id.contains(it) }) return i + } + return layers.length() + } + + private val SDK_OVERLAY_LAYER_HINTS = listOf("shape", "marker", "route") + /** JSONArray への位置指定挿入(org.json は insert が無いため末尾に足してからシフト)。 */ private fun insertLayerAt( layers: JSONArray,