Skip to content
Open
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
27 changes: 27 additions & 0 deletions api/android-sdk-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,13 @@ public final class com.mapconductor.core.controller.OverlayKind extends java.lan
public static com.mapconductor.core.controller.OverlayKind valueOf(java.lang.String);
public static kotlin.enums.EnumEntries<com.mapconductor.core.controller.OverlayKind> getEntries();
}
public final class com.mapconductor.core.controller.OverlayLayerStack {
public com.mapconductor.core.controller.OverlayLayerStack();
public final void put(java.lang.String, com.mapconductor.core.controller.OverlayStacking$Key);
public final void remove(java.lang.String);
public final com.mapconductor.core.controller.OverlayStacking$Key keyOf(java.lang.String);
public final java.lang.String beforeId(java.util.List<java.lang.String>, com.mapconductor.core.controller.OverlayStacking$Key);
}
public interface com.mapconductor.core.controller.OverlayRendererInterface<ActualType, StateType, EntityType> {
public abstract java.lang.Object onAdd(java.util.List<? extends StateType>, kotlin.coroutines.Continuation<? super java.util.List<? extends ActualType>>);
public abstract java.lang.Object onChange(java.util.List<? extends com.mapconductor.core.controller.OverlayRendererInterface$ChangeParamsInterface<EntityType>>, kotlin.coroutines.Continuation<? super java.util.List<? extends ActualType>>);
Expand All @@ -362,6 +369,26 @@ public interface com.mapconductor.core.controller.OverlayRendererInterface$Chang
public abstract EntityType getCurrent();
public abstract EntityType getPrev();
}
public final class com.mapconductor.core.controller.OverlayStacking {
public static final com.mapconductor.core.controller.OverlayStacking INSTANCE;
public static final float BAND_WIDTH;
public final java.util.List<com.mapconductor.core.controller.OverlayKind> getBOTTOM_TO_TOP();
public final int band(com.mapconductor.core.controller.OverlayKind);
public final float flatZIndex(com.mapconductor.core.controller.OverlayKind, int);
public final <L> L insertBefore(java.util.List<? extends L>, com.mapconductor.core.controller.OverlayStacking$Key, kotlin.jvm.functions.Function1<? super L, com.mapconductor.core.controller.OverlayStacking$Key>);
}
public final class com.mapconductor.core.controller.OverlayStacking$Key implements java.lang.Comparable<com.mapconductor.core.controller.OverlayStacking$Key> {
public final com.mapconductor.core.controller.OverlayKind getKind();
public final int getZIndex();
public int compareTo(com.mapconductor.core.controller.OverlayStacking$Key);
public final com.mapconductor.core.controller.OverlayKind component1();
public final int component2();
public final com.mapconductor.core.controller.OverlayStacking$Key copy(com.mapconductor.core.controller.OverlayKind, int);
public java.lang.String toString();
public int hashCode();
public boolean equals(java.lang.Object);
public int compareTo(java.lang.Object);
}
public final class com.mapconductor.core.controller.SlottedOverlayController$DefaultImpls {
public static <StateType, EntityType> boolean has(com.mapconductor.core.controller.SlottedOverlayController<StateType, EntityType>, java.lang.String);
public static <StateType, EntityType> void setClickListenerAny(com.mapconductor.core.controller.SlottedOverlayController<StateType, EntityType>, java.lang.Object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ abstract class CircleController<ActualCircle>(
CircleState,
CircleEntityInterface<ActualCircle>,
> {
override val zIndex: Int = 3
override val zIndex: Int = 4
val semaphore = Semaphore(1)

var clickListener: OnCircleEventHandler? = null
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/com/mapconductor/core/controller/OverlayKind.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ package com.mapconductor.core.controller
* ジェネリクスは実行時に消えるので状態の型では判別できない。かといって `Class` を
* 持たせると、状態型がプロバイダ固有になったときに破綻する。種別という別の軸で持つ。
*
* **描画順(zIndex)とは別軸**であることに注意。現状 polygon(3) > groundImage(2) だが、
* クリックの探索は groundImage が先。
* **描画順とは別軸**であることに注意。描画は [OverlayStacking] の帯
* (ラスター < GroundImage < ポリゴン < サークル < ポリライン < マーカー)で決まるが、
* クリックの探索は groundImage がポリゴンより先。
*/
enum class OverlayKind {
Marker,
Expand Down
134 changes: 134 additions & 0 deletions src/main/java/com/mapconductor/core/controller/OverlayStacking.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.mapconductor.core.controller

import kotlin.math.abs

/**
* オーバーレイの重なり順(すべてのプロバイダ・3 プラットフォーム共通の決まり)。
*
* 下から順に:
* 1. ラスターレイヤー(RasterLayerState、アプリが足すタイル)
* 2. GroundImage
* 3. ポリゴン(ネイティブのポリゴンと、SDK のポリゴンのタイルレイヤーが同じ帯)
* 4. サークル
* 5. ポリライン
* 6. マーカー(ネイティブのマーカーと、SDK のマーカーのタイルレイヤーが同じ帯)
*
* 帯の中は zIndex の小さい順、同じ zIndex なら後から渡したものが上。
* 追加した順・送り直した順(clearOverlays の後など)・デザイン変更の後に左右されない。
*
* ネイティブ SDK の重なりの持ち方は 2 通りあり、それぞれに 1 つずつ道具を用意する:
* - 種類をまたいで比べられる 1 本の数値(Google Maps の zIndex など)→ [flatZIndex]
* - スタイルの層の並び(MapLibre / Mapbox などの GL 系)→ [insertBefore]
*/
object OverlayStacking {
/** 下から上への帯の並び。 */
val BOTTOM_TO_TOP: List<OverlayKind> =
listOf(
OverlayKind.RasterLayer,
OverlayKind.GroundImage,
OverlayKind.Polygon,
OverlayKind.Circle,
OverlayKind.Polyline,
OverlayKind.Marker,
)

/** 帯の番号(0 が一番下)。 */
fun band(kind: OverlayKind): Int = BOTTOM_TO_TOP.indexOf(kind)

/**
* 帯の幅。[flatZIndex] は `band * BAND_WIDTH` を中心に、zIndex を
* `±BAND_WIDTH / 2` 未満へ押し込める。
*/
const val BAND_WIDTH: Float = 1000f

// zIndex が小さいうちはほぼ 1 刻みで並び、大きくなっても帯の外へ出ない。
private const val SQUASH_SCALE = 100_000.0

/**
* 種類をまたいで 1 本の数値で重なりを比べる SDK 向けの値。
*
* 帯ごとに [BAND_WIDTH] の区間を割り当て、zIndex(Int 全域)を単調に
* その区間の内側へ押し込める。どんな zIndex を渡しても下の帯の上へは出ない。
* float の精度上、zIndex の差が非常に大きい(数十万以上)ところでは
* 隣り合う値が同じになることがある(その場合の順はネイティブ SDK 次第)。
*/
fun flatZIndex(
kind: OverlayKind,
zIndex: Int,
): Float {
val z = zIndex.toDouble()
val squashed = z / (abs(z) + SQUASH_SCALE) * (BAND_WIDTH / 2.0) * 0.999
return (band(kind) * BAND_WIDTH + squashed).toFloat()
}

/**
* 重なりの並びを決める鍵。帯 → zIndex の順で比べる。
* 同じ鍵どうしは、後から入れたものが上([insertBefore] がそう選ぶ)。
*/
data class Key(
val kind: OverlayKind,
val zIndex: Int = 0,
) : Comparable<Key> {
override fun compareTo(other: Key): Int {
val b = band(kind).compareTo(band(other.kind))
if (b != 0) return b
return zIndex.compareTo(other.zIndex)
}
}

/**
* 層の並び(GL 系のスタイルなど)に新しい層を入れるとき、どの層の**下**に入れるか。
*
* [layersBottomToTop] は今の並び(下から上)。[keyOf] は SDK が足した層なら鍵を、
* ベースマップなど SDK のものでない層なら null を返す。
*
* 鍵が [key] より大きい最初の(一番下の)SDK の層を返す。無ければ null
* (呼び出し側は一番上へ足す)。同じ鍵の層の上に入るので、同じ zIndex なら
* 後から渡したものが上になる。
*
* 並びを持たず毎回スタイルから読むので、スタイルの読み直しや送り直しの後でも
* 結果が変わらない。
*/
fun <L> insertBefore(
layersBottomToTop: List<L>,
key: Key,
keyOf: (L) -> Key?,
): L? =
layersBottomToTop.firstOrNull { layer ->
val k = keyOf(layer) ?: return@firstOrNull false
k > key
}
}

/**
* 層の id で重なりを持つ SDK(GL 系のスタイル)向けの台帳。
*
* SDK が足した層の id と [OverlayStacking.Key] の対応を持ち、新しい層を入れる位置
* (どの層の下に入れるか)を [beforeId] で返す。台帳に無い id はベースマップの層として
* 無視する。スタイルを読み直して層が消えても台帳は残るので、足し直すときも同じ位置になる。
*/
class OverlayLayerStack {
private val keys = java.util.concurrent.ConcurrentHashMap<String, OverlayStacking.Key>()

fun put(
layerId: String,
key: OverlayStacking.Key,
) {
keys[layerId] = key
}

fun remove(layerId: String) {
keys.remove(layerId)
}

fun keyOf(layerId: String): OverlayStacking.Key? = keys[layerId]

/**
* [key] の層を、今の並び [layerIdsBottomToTop](下から上)のどの層の下に入れるか。
* null なら一番上へ足す。
*/
fun beforeId(
layerIdsBottomToTop: List<String>,
key: OverlayStacking.Key,
): String? = OverlayStacking.insertBefore(layerIdsBottomToTop, key) { keys[it] }
}
127 changes: 127 additions & 0 deletions src/test/java/com/mapconductor/core/controller/OverlayStackingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.mapconductor.core.controller

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* 重なり順の決まり(ラスター < GroundImage < ポリゴン < サークル < ポリライン < マーカー)。
* ios-sdk-core / js-sdk-core の同名テストと同じ表。
*/
class OverlayStackingTest {
@Test
fun `帯は下からラスター・GroundImage・ポリゴン・サークル・ポリライン・マーカー`() {
assertEquals(
listOf(
OverlayKind.RasterLayer,
OverlayKind.GroundImage,
OverlayKind.Polygon,
OverlayKind.Circle,
OverlayKind.Polyline,
OverlayKind.Marker,
),
OverlayKind.entries.sortedBy { OverlayStacking.band(it) },
)
}

@Test
fun `flatZIndex はどんな zIndex でも帯をまたがない`() {
val extremes = listOf(Int.MIN_VALUE, -35_000_000, -1, 0, 1, 1000, Int.MAX_VALUE)
val bands = OverlayStacking.BOTTOM_TO_TOP
for (i in 0 until bands.size - 1) {
val lowerMax = extremes.maxOf { OverlayStacking.flatZIndex(bands[i], it) }
val upperMin = extremes.minOf { OverlayStacking.flatZIndex(bands[i + 1], it) }
assertTrue("${bands[i]} must stay below ${bands[i + 1]}", lowerMax < upperMin)
}
}

@Test
fun `flatZIndex は帯の中で zIndex の順を保つ`() {
val zs = listOf(-1000, -10, -1, 0, 1, 2, 10, 1000)
val flat = zs.map { OverlayStacking.flatZIndex(OverlayKind.Polygon, it) }
assertEquals(flat.sorted(), flat)
assertEquals(flat.size, flat.toSet().size)
}

private data class L(
val id: String,
val key: OverlayStacking.Key?,
)

private fun stack(vararg layers: L) = layers.toList()

private fun k(
kind: OverlayKind,
z: Int = 0,
) = OverlayStacking.Key(kind, z)

@Test
fun `insertBefore は鍵が大きい一番下の SDK の層を選び、ベースマップの層は無視する`() {
val layers =
stack(
L("basemap-fill", null),
L("raster", k(OverlayKind.RasterLayer)),
L("basemap-labels", null),
L("polygon", k(OverlayKind.Polygon)),
L("polyline", k(OverlayKind.Polyline)),
L("markers", k(OverlayKind.Marker)),
)
val before = OverlayStacking.insertBefore(layers, k(OverlayKind.GroundImage)) { it.key }
assertEquals("polygon", before?.id)
assertEquals(
"polyline",
OverlayStacking.insertBefore(layers, k(OverlayKind.Circle)) { it.key }?.id,
)
assertNull(OverlayStacking.insertBefore(layers, k(OverlayKind.Marker, 5)) { it.key })
}

@Test
fun `同じ鍵なら後から入れたものが上、zIndex が大きいものはその上`() {
val layers =
stack(
L("pg-0a", k(OverlayKind.Polygon, 0)),
L("pg-0b", k(OverlayKind.Polygon, 0)),
L("pg-3", k(OverlayKind.Polygon, 3)),
L("circle", k(OverlayKind.Circle)),
)
assertEquals("pg-3", OverlayStacking.insertBefore(layers, k(OverlayKind.Polygon, 0)) { it.key }?.id)
assertEquals("pg-3", OverlayStacking.insertBefore(layers, k(OverlayKind.Polygon, 1)) { it.key }?.id)
assertEquals("circle", OverlayStacking.insertBefore(layers, k(OverlayKind.Polygon, 3)) { it.key }?.id)
assertEquals("pg-0a", OverlayStacking.insertBefore(layers, k(OverlayKind.Polygon, -1)) { it.key }?.id)
}

@Test
fun `追加の順によらず同じ並びになる`() {
val kinds = OverlayStacking.BOTTOM_TO_TOP
listOf(kinds, kinds.reversed(), listOf(kinds[3], kinds[0], kinds[5], kinds[1], kinds[4], kinds[2])).forEach { order ->
val layers = mutableListOf<L>()
order.forEach { kind ->
val key = k(kind)
val before = OverlayStacking.insertBefore(layers, key) { it.key }
val at = if (before == null) layers.size else layers.indexOf(before)
layers.add(at, L(kind.name, key))
}
assertEquals(kinds.map { it.name }, layers.map { it.id })
}
}
}

class OverlayLayerStackTest {
@Test
fun `台帳に無い層は無視し、スタイルを読み直しても同じ位置を返す`() {
val stack = OverlayLayerStack()
stack.put("raster-layer-A", OverlayStacking.Key(OverlayKind.RasterLayer))
stack.put("polygon-fill-layer-0", OverlayStacking.Key(OverlayKind.Polygon, 0))
stack.put("markers-layer", OverlayStacking.Key(OverlayKind.Marker))
val layers = listOf("water", "raster-layer-A", "road-label", "polygon-fill-layer-0", "markers-layer")
assertEquals(
"polygon-fill-layer-0",
stack.beforeId(layers, OverlayStacking.Key(OverlayKind.GroundImage)),
)
// 読み直し直後(SDK の層がまだ何も無い)なら一番上。
assertNull(stack.beforeId(listOf("water", "road-label"), OverlayStacking.Key(OverlayKind.GroundImage)))
stack.remove("markers-layer")
assertNull(stack.beforeId(layers, OverlayStacking.Key(OverlayKind.Polyline)))
}
}
Loading