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
13 changes: 13 additions & 0 deletions api/android-for-maptiler.api
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ final class com.mapconductor.maptiler.MapTilerMapViewSaver$Companion {
public final class com.mapconductor.maptiler.MapTilerMapViewScope extends com.mapconductor.compose.MapViewScope {
public com.mapconductor.maptiler.MapTilerMapViewScope();
}
public final class com.mapconductor.maptiler.MapTilerOverlayStacking {
public static final com.mapconductor.maptiler.MapTilerOverlayStacking INSTANCE;
public final com.mapconductor.core.controller.OverlayStacking$Key key(com.mapconductor.core.controller.OverlayKind, int);
public final com.mapconductor.core.controller.OverlayStacking$Key getMARKER_TILE();
public final void add(com.maptiler.maptilersdk.map.style.MTStyle, com.maptiler.maptilersdk.map.style.layer.MTLayer, com.mapconductor.core.controller.OverlayStacking$Key);
public final void remove(com.maptiler.maptilersdk.map.style.MTStyle, java.lang.String);
public final void onIdle(com.maptiler.maptilersdk.map.style.MTStyle);
public final void reset(com.maptiler.maptilersdk.map.style.MTStyle);
}
final class com.mapconductor.maptiler.MapTilerOverlayStacking$Entry {
public final java.lang.String getId();
public final com.mapconductor.core.controller.OverlayStacking$Key getKey();
}
public final class com.mapconductor.maptiler.MapTilerProjectedAnnotationKt {
public static final void MapTilerProjectedAnnotation(com.maptiler.maptilersdk.map.MTMapViewController, com.maptiler.maptilersdk.map.LngLat, com.maptiler.maptilersdk.map.types.MTPoint, androidx.compose.ui.Alignment, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, androidx.compose.runtime.Composer, int, int);
}
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/com/mapconductor/maptiler/MapTilerMapView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,13 @@ fun MapTilerMapSurface(
// 非 Compose ホストが `setMapDesignType` を直接呼んだ場合は
// 調整が落ちたままになる。
mutations.onStyleLoaded()
// 重なり順の帯の順(下から)に足し直す。
mtController.style?.let { MapTilerOverlayStacking.reset(it) }
controller.reapplyRasterLayers()
controller.reapplyPolylines()
controller.reapplyGroundImages()
controller.reapplyPolygons()
controller.reapplyCircles()
controller.reapplyGroundImages()
controller.reapplyPolylines()
controller.mapLoaded.value = true
controller.dispatchInitialCameraToOverlays()
onLoaded?.invoke(currentState)
Expand Down Expand Up @@ -383,6 +385,13 @@ fun MapTilerMapSurface(
onMoveEnd?.invoke(pos)
}

MTEvent.ON_IDLE ->
// SDK はこのあとで待たせていた層を送る。1 度メインへ戻してから
// 並びを送り直す(MapTilerOverlayStacking.onIdle)。
mtController.style?.let { style ->
coroutineScope.launch { MapTilerOverlayStacking.onIdle(style) }
}

else -> Unit
}
}
Expand Down
108 changes: 108 additions & 0 deletions src/main/java/com/mapconductor/maptiler/MapTilerOverlayStacking.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.mapconductor.maptiler

import com.mapconductor.core.controller.OverlayKind
import com.mapconductor.core.controller.OverlayStacking
import com.maptiler.maptilersdk.map.style.MTStyle
import com.maptiler.maptilersdk.map.style.layer.MTLayer
import java.util.WeakHashMap

/**
* 重なり順([OverlayStacking]: ラスター < GroundImage < ポリゴン < サークル < ポリライン < マーカー)。
*
* MapTiler SDK の `MTStyle.addLayer` は beforeId を取らず、常に一番上へ積む。並びを読む
* API も無い。そこで SDK が足した層の並びをここで持ち、足した直後に `moveLayer` で
* 「鍵が自分より大きい一番下の層」の下へ動かす。ベースマップの層は見えないので、
* ラスターもベースマップ全体(ラベルを含む)の上に乗る(GL 系の他プロバイダはラベルの下)。
*
* `MTStyle.addLayer` はすぐにはページへ届かない。画像ソース以外は `isSourceLoaded` の
* 往復を待ち、ソースが読み込み前なら次の `idle` まで SDK の中で待たされる。その前に
* `moveLayer` が届くと、まだ無い層を動かそうとして何も起きない(逆の順に足すとラスターが
* 一番上に残る)。そこで足したあとの `idle` で、記録した並びをもう一度ページへ送る
* ([onIdle]、MapTilerMapView の onEventTriggered)。
*
* スタイルを読み直すと層は消える。[reset] で「今スタイルにある層」を空にしてから
* 帯の順に足し直す(MapTilerMapView の onMapViewInitialized)。
*/
internal object MapTilerOverlayStacking {
private class Entry(
val id: String,
val key: OverlayStacking.Key,
)

/** スタイルにある SDK の層(下から上)。 */
private val stacks = WeakHashMap<MTStyle, MutableList<Entry>>()

/** 並びを送り直す残りの `idle` の回数。足すたびに [RESTACK_IDLES] へ戻す。 */
private val pendingIdles = WeakHashMap<MTStyle, Int>()

/**
* 足したあと何回の `idle` で並びを送り直すか。1 回目の `idle` で SDK が待たせていた層を
* ページへ送る。送り直し自体が描き直しを呼ぶので、2 回目で遅れて届いた層も拾って止まる。
*/
private const val RESTACK_IDLES = 2

fun key(
kind: OverlayKind,
zIndex: Int = 0,
) = OverlayStacking.Key(kind, zIndex)

/** マーカーのタイル: マーカーの帯の一番下。 */
val MARKER_TILE = key(OverlayKind.Marker, Int.MIN_VALUE)

fun add(
style: MTStyle,
layer: MTLayer,
key: OverlayStacking.Key,
) {
val id = layer.identifier
val before =
synchronized(stacks) {
val stack = stacks.getOrPut(style) { mutableListOf() }
stack.removeAll { it.id == id }
val b = OverlayStacking.insertBefore(stack, key) { it.key }
val at = if (b == null) stack.size else stack.indexOf(b)
stack.add(at, Entry(id, key))
pendingIdles[style] = RESTACK_IDLES
b?.id
}
style.addLayer(layer)
if (before != null) runCatching { style.moveLayer(id, before) }
}

fun remove(
style: MTStyle,
layerId: String,
) {
synchronized(stacks) { stacks[style]?.removeAll { it.id == layerId } }
}

/**
* 地図が `idle` になった。足したばかりの層があれば、記録した並び(下から上)を
* 上から順に `moveLayer` で送り直す。まだページに無い層の `moveLayer` は何もしない。
*
* SDK は `idle` を委譲先へ渡した**あと**で、待たせていた層を送る。ここをそのまま
* 呼ぶと `moveLayer` が層より先に届くので、呼び出し側は 1 度メインスレッドへ戻して
* から呼ぶこと。
*/
fun onIdle(style: MTStyle) {
val ids =
synchronized(stacks) {
val left = pendingIdles[style] ?: return
if (left <= 1) pendingIdles.remove(style) else pendingIdles[style] = left - 1
stacks[style]?.map { it.id }.orEmpty()
}
if (ids.isEmpty()) return
runCatching { style.moveLayer(ids.last()) }
for (i in ids.size - 2 downTo 0) {
runCatching { style.moveLayer(ids[i], ids[i + 1]) }
}
}

/** スタイルを読み直した(SDK の層が全部消えた)。 */
fun reset(style: MTStyle) {
synchronized(stacks) {
stacks[style]?.clear()
pendingIdles.remove(style)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,12 @@ internal class MapTilerStyleMutationTarget(

override fun removeLayer(layerId: String) {
val target = style() ?: return
// `removeLayerById` reaches the page without being checked against
// anything, and removing an absent layer there is a no-op.
target.removeLayerById(layerId)
// Remove it from MTStyle's record too: `removeLayerById` alone leaves
// the id recorded, and adding the same background layer again throws
// LayerAlreadyExists. A layer MTStyle never recorded falls back to
// `removeLayerById`, which reaches the page unchecked (a no-op when the
// layer is absent). See MapTilerStyleRemoval.kt.
target.removeLayerAndForget(MTBackgroundLayer(layerId))
own(target) -= layerId
}

Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/mapconductor/maptiler/MapTilerStyleRemoval.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.mapconductor.maptiler

import com.mapconductor.core.InternalMapConductorApi
import com.maptiler.maptilersdk.map.style.MTStyle
import com.maptiler.maptilersdk.map.style.layer.MTLayer
import com.maptiler.maptilersdk.map.style.source.MTSource

/*
* MTStyle からレイヤ・ソースを外すときは、id ではなくオブジェクトで外す。
*
* MTStyle は addLayer / addSource したものを id で覚えていて、同じ id をもう一度足そうとすると
* LayerAlreadyExists / SourceAlreadyExists を投げる。ところが removeLayerById / removeSourceById は
* ページ(MapLibre GL JS)から消すだけで、この記録を消さない。id で外したポリゴン・円・ラスター等を
* 同じ id(= 同じ state.id)で戻すと add が投げて、地図に戻らなかった。
* removeLayer(layer) / removeSource(source) は記録も消してからページへ渡す。
*
* 記録に無いとき(add の記録は SDK のコルーチンで遅れて入る)は NotFound を投げるので、
* そのときは id でページから消す。
*/

@InternalMapConductorApi
internal fun MTStyle.removeLayerAndForget(layer: MTLayer) {
runCatching { removeLayer(layer) }
.onFailure { runCatching { removeLayerById(layer.identifier) } }
}

@InternalMapConductorApi
internal fun MTStyle.removeSourceAndForget(source: MTSource) {
runCatching { removeSource(source) }
.onFailure { runCatching { removeSourceById(source.identifier) } }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import androidx.compose.ui.graphics.toArgb
import com.mapconductor.core.circle.CircleEntityInterface
import com.mapconductor.core.circle.CircleOverlayRendererInterface
import com.mapconductor.core.circle.CircleState
import com.mapconductor.core.controller.OverlayKind
import com.mapconductor.core.geometry.OverlayGeoJson
import com.mapconductor.core.geometry.circleToRing
import com.mapconductor.maptiler.MapTilerOverlayStacking
import com.mapconductor.maptiler.removeLayerAndForget
import com.mapconductor.maptiler.removeSourceAndForget
import com.maptiler.maptilersdk.map.MTMapViewController
import com.maptiler.maptilersdk.map.style.dsl.PropertyValue
import com.maptiler.maptilersdk.map.style.layer.MTLayerVisibility
Expand Down Expand Up @@ -66,16 +70,24 @@ class MapTilerCircleOverlayRenderer(
val style = mtController.style ?: return
handles.forEach { handle ->
runCatching { style.addSource(handle.source) }
runCatching { style.addLayer(handle.fillLayer) }
runCatching { style.addLayer(handle.lineLayer) }
runCatching { MapTilerOverlayStacking.add(style, handle.fillLayer, stackKey(handle.fillLayer.identifier)) }
runCatching { MapTilerOverlayStacking.add(style, handle.lineLayer, stackKey(handle.lineLayer.identifier)) }
}
}

/** 層の id → zIndex(帯の中の順)。handle は公開型なので持たせずここで覚える。 */
private val zIndexByLayer = java.util.concurrent.ConcurrentHashMap<String, Int>()

/** サークルの帯。同じ鍵なら後から足したもの(輪郭)が上。 */
private fun stackKey(layerId: String) = MapTilerOverlayStacking.key(OverlayKind.Circle, zIndexByLayer[layerId] ?: 0)

private fun addCircle(state: CircleState): MapTilerCircleHandle? {
val json = buildGeoJson(state) ?: return null
val sourceId = "circle-source-${state.id}"
val fillLayerId = "circle-fill-${state.id}"
val lineLayerId = "circle-line-${state.id}"
zIndexByLayer[fillLayerId] = state.zIndex ?: 0
zIndexByLayer[lineLayerId] = state.zIndex ?: 0
val source = MTGeoJSONSource.fromJsonString(sourceId, json)

val fillLayer =
Expand All @@ -99,9 +111,9 @@ class MapTilerCircleOverlayRenderer(
val style = mtController.style ?: return handle
runCatching { style.addSource(source) }
.onFailure { Log.w(TAG, "addSource failed: ${it.message}") }
runCatching { style.addLayer(fillLayer) }
runCatching { MapTilerOverlayStacking.add(style, fillLayer, stackKey(fillLayer.identifier)) }
.onFailure { Log.w(TAG, "addLayer(fill) failed: ${it.message}") }
runCatching { style.addLayer(lineLayer) }
runCatching { MapTilerOverlayStacking.add(style, lineLayer, stackKey(lineLayer.identifier)) }
.onFailure { Log.w(TAG, "addLayer(line) failed: ${it.message}") }
return handle
}
Expand Down Expand Up @@ -141,9 +153,12 @@ class MapTilerCircleOverlayRenderer(
private fun removeCircle(entity: CircleEntityInterface<MapTilerCircleHandle>) {
val style = mtController.style ?: return
val handle = entity.circle
runCatching { style.removeLayerById(handle.lineLayerId) }
runCatching { style.removeLayerById(handle.fillLayerId) }
runCatching { style.removeSourceById(handle.sourceId) }
// id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。
MapTilerOverlayStacking.remove(style, handle.lineLayer.identifier)
style.removeLayerAndForget(handle.lineLayer)
MapTilerOverlayStacking.remove(style, handle.fillLayer.identifier)
style.removeLayerAndForget(handle.fillLayer)
style.removeSourceAndForget(handle.source)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.mapconductor.maptiler.groundimage

import com.mapconductor.core.controller.OverlayKind
import com.mapconductor.core.features.GeoRectBounds
import com.mapconductor.core.groundimage.GroundImageEntityInterface
import com.mapconductor.core.groundimage.GroundImageOverlayRendererInterface
import com.mapconductor.core.groundimage.GroundImageState
import com.mapconductor.maptiler.MapTilerOverlayStacking
import com.mapconductor.maptiler.removeLayerAndForget
import com.mapconductor.maptiler.removeSourceAndForget
import com.maptiler.maptilersdk.map.LngLat
import com.maptiler.maptilersdk.map.MTMapViewController
import com.maptiler.maptilersdk.map.style.dsl.PropertyValue
Expand Down Expand Up @@ -56,14 +60,21 @@ class MapTilerGroundImageOverlayRenderer(
val style = mtController.style ?: return
handles.forEach { handle ->
runCatching { style.addSource(handle.source) }
runCatching { style.addLayer(handle.layer) }
runCatching { MapTilerOverlayStacking.add(style, handle.layer, stackKey(handle.layer.identifier)) }
}
}

/** 層の id → zIndex(帯の中の順)。handle は公開型なので持たせずここで覚える。 */
private val zIndexByLayer = java.util.concurrent.ConcurrentHashMap<String, Int>()

/** GroundImage の帯(ラスターの上、ポリゴンの下)。 */
private fun stackKey(layerId: String) = MapTilerOverlayStacking.key(OverlayKind.GroundImage, zIndexByLayer[layerId] ?: 0)

private fun addGroundImage(state: GroundImageState): MapTilerGroundImageHandle? {
val coordinates = state.bounds.toImageCoordinates() ?: return null
val sourceId = "groundimage-source-${state.id}"
val layerId = "groundimage-layer-${state.id}"
zIndexByLayer[layerId] = 0
val source = MTImageSource(sourceId, imageUrl(state.image), coordinates)
val layer =
MTRasterLayer(layerId, sourceId).apply {
Expand All @@ -85,7 +96,7 @@ class MapTilerGroundImageOverlayRenderer(
val style = mtController.style ?: return handle
runCatching { style.addSource(source) }
.onFailure { Log.w(TAG, "addSource failed: ${it.message}") }
runCatching { style.addLayer(layer) }
runCatching { MapTilerOverlayStacking.add(style, layer, stackKey(layer.identifier)) }
.onFailure { Log.w(TAG, "addLayer failed: ${it.message}") }
return handle
}
Expand Down Expand Up @@ -132,8 +143,10 @@ class MapTilerGroundImageOverlayRenderer(
private fun removeGroundImage(entity: GroundImageEntityInterface<MapTilerGroundImageHandle>) {
val style = mtController.style ?: return
val handle = entity.groundImage
runCatching { style.removeLayerById(handle.layerId) }
runCatching { style.removeSourceById(handle.sourceId) }
// id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。
MapTilerOverlayStacking.remove(style, handle.layer.identifier)
style.removeLayerAndForget(handle.layer)
style.removeSourceAndForget(handle.source)
}

/**
Expand Down
Loading
Loading