Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/android-for-tomtom.api
Original file line number Diff line number Diff line change
Expand Up @@ -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<? super kotlin.Unit>);
public void setMarkerAnimationOverlayHost(com.mapconductor.core.marker.MarkerAnimationOverlayHost);
public java.lang.Object compositionGroundImages(java.util.List<com.mapconductor.core.groundimage.GroundImageState>, kotlin.coroutines.Continuation<? super kotlin.Unit>);
public java.lang.Object compositionPolygons(java.util.List<com.mapconductor.core.polygon.PolygonState>, kotlin.coroutines.Continuation<? super kotlin.Unit>);
public java.lang.Object compositionCircles(java.util.List<com.mapconductor.core.circle.CircleState>, kotlin.coroutines.Continuation<? super kotlin.Unit>);
public java.lang.Object compositionPolylines(java.util.List<com.mapconductor.core.polyline.PolylineState>, kotlin.coroutines.Continuation<? super kotlin.Unit>);
public boolean hasPolyline(com.mapconductor.core.polyline.PolylineState);
public void setOnPolylineClickListener(kotlin.jvm.functions.Function1<? super com.mapconductor.core.polyline.PolylineEvent, kotlin.Unit>);
public boolean hasPolygon(com.mapconductor.core.polygon.PolygonState);
Expand Down
68 changes: 68 additions & 0 deletions src/main/java/com/mapconductor/tomtom/TomTomMapViewController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<GroundImageState>) {
val added = data.any { !groundImageController.groundImageManager.hasEntity(it.id) }
groundImageController.add(data)
if (added) {
restackPolygons()
restackCircles()
}
}

override suspend fun compositionPolygons(data: List<PolygonState>) {
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<CircleState>) {
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<PolylineState>) {
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)
Expand Down Expand Up @@ -345,6 +409,9 @@ class TomTomMapViewController internal constructor(
// 注意: 合成は現状 browsing/light ベース固定。Standard 以外のデザインでラスタを載せた場合は
// ベースが browsing になる(対象ページは Standard のため実用上問題なし)。
internal val composedRasterLayers = LinkedHashMap<String, TomTomStyleComposer.RasterSpec>()

/** アプリのラスターの zIndex(帯の中の順)。[composedRasterLayers] と同じキー。 */
internal val composedRasterZIndex = java.util.concurrent.ConcurrentHashMap<String, Int>()
internal val composedStyleMutex = Mutex()
internal var rasterApiKey: String? = null
internal var rasterCacheDir: File? = null
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RasterSpec>,
aboveOverlays: List<RasterSpec>,
outFile: File,
withoutBasemap: Boolean,
): Uri? =
withContext(Dispatchers.IO) {
// The SDK will not take a style that has none of its own layers
Expand Down Expand Up @@ -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))
Expand All @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down
Loading