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
113 changes: 113 additions & 0 deletions src/androidTest/java/com/mapconductor/mapbox/MapboxMapViewSaverTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.mapconductor.mapbox

import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.mapconductor.core.features.GeoPoint
import com.mapconductor.core.map.AttributionRule
import com.mapconductor.core.map.MapCameraPosition
import com.mapconductor.core.map.MapDesignTypeInterface
import com.mapconductor.core.map.MapUISettings
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import android.os.Bundle
import android.os.Parcel

/**
* 回転などの構成変更で state が [MapboxMapViewSaver] を通って作り直されても、デザイン(id・
* attributionRules ほか)と uiSettings・カメラが落ちないこと。Bundle は実機の Parcel を一度通す。
*/
@RunWith(AndroidJUnit4::class)
class MapboxMapViewSaverTest {
private val rules =
listOf(AttributionRule("© test"), AttributionRule("© ranged", minZoom = 3, maxZoom = 15))

private val camera =
MapCameraPosition(
position = GeoPoint(21.38, -157.93),
zoom = 12.0,
bearing = 20.0,
tilt = 10.0,
)

@Test
fun keepsCustomDesignWithAttributionRules() {
val design = MapboxMapDesign.Custom("my-account/my-style", rules)
val state = MapboxViewState(mapDesignType = design, id = "mapbox-1", cameraPosition = camera)
state.uiSettings = MapUISettings(rotateGesture = false, tiltGesture = false)

val restored = roundTrip(MapboxMapViewSaver().createSaver(), state)

val actual = restored.mapDesignType
assertTrue(actual is MapboxMapDesign.Custom)
assertEquals("my-account/my-style", actual.id)
assertEquals(rules, actual.attributionRules)
assertCommon(restored, "mapbox-1")
}

@Test
fun keepsStyleUriAsStyleUri() {
// 以前は id だけを保存して create(id) で戻しており、URI が mapbox://styles/mapbox/<URI> の
// Custom になっていた。
val design = MapboxMapDesign.StyleUri("https://example.com/style.json", rules)
val restored = roundTrip(MapboxMapViewSaver().createSaver(), MapboxViewState(design, "mapbox-2"))

val actual = restored.mapDesignType
assertTrue(actual is MapboxMapDesign.StyleUri)
assertEquals("https://example.com/style.json", actual.getValue())
assertEquals(rules, actual.attributionRules)
}

@Test
fun keepsPresetDesignAsTheSameObject() {
val restored = roundTrip(MapboxMapViewSaver().createSaver(), MapboxViewState(MapboxMapDesign.Dark, "mapbox-3"))
assertSame(MapboxMapDesign.Dark, restored.mapDesignType)
}

@Test
fun appImplementationComesBackWithItsStyle() {
val design =
object : MapDesignTypeInterface<String> {
override val id: String = "app-design"
override val attributionRules: List<AttributionRule> = rules

override fun getValue(): String = "${MapboxMapDesign.MAPBOX_URL}/streets-v12"
}
val restored = roundTrip(MapboxMapViewSaver().createSaver(), MapboxViewState(design, "mapbox-4"))

assertEquals(design.getValue(), restored.mapDesignType.getValue())
assertEquals(rules, restored.mapDesignType.attributionRules)
}

private fun assertCommon(
restored: com.mapconductor.core.map.MapViewStateInterface<*>,
id: String,
) {
assertEquals(id, restored.id)
assertEquals(false, restored.uiSettings.rotateGesture)
assertEquals(false, restored.uiSettings.tiltGesture)
assertEquals(true, restored.uiSettings.scrollGesture)
assertEquals(12.0, restored.cameraPosition.zoom, 1e-9)
assertEquals(20.0, restored.cameraPosition.bearing, 1e-9)
assertEquals(21.38, restored.cameraPosition.position.latitude, 1e-9)
}

private fun <T : Any> roundTrip(
saver: Saver<T, Bundle>,
value: T,
): T {
val saved = with(saver) { SaverScope { true }.save(value) }!!
// 一度 Parcel に書いて読み戻す(プロセスが落ちたあとの復元と同じ経路)。
val parcel = Parcel.obtain()
try {
parcel.writeBundle(saved)
parcel.setDataPosition(0)
return saver.restore(parcel.readBundle(javaClass.classLoader)!!)!!
} finally {
parcel.recycle()
}
}
}
53 changes: 48 additions & 5 deletions src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.mapconductor.compose.map.BaseMapViewSaver
import com.mapconductor.core.map.MapCameraPosition
import com.mapconductor.core.map.MapCameraPositionInterface
import com.mapconductor.core.map.MapViewState
import com.mapconductor.core.map.MapViewStateBundle
import com.mapconductor.core.map.MapViewStateInterface
import com.mapconductor.mapbox.MapboxMapDesign.Standard
import java.util.UUID
Expand Down Expand Up @@ -39,6 +40,9 @@ class MapboxViewState(
}

internal fun onMapDesignTypeChange(value: MapboxDesignType) {
// 地図から読み戻した値はスタイル URI から作ったデザインで、attributionRules を持たない。
// 同じスタイルなら、アプリが渡した(あるいは復元した)デザインを残す。
if (value.getValue() == _mapDesignType.getValue()) return
_mapDesignType = value
}

Expand All @@ -55,7 +59,20 @@ class MapboxMapViewSaver : BaseMapViewSaver<MapboxViewState>() {
state: MapboxViewState,
bundle: Bundle,
) {
bundle.putString("id", state.mapDesignType.id)
val design = state.mapDesignType
bundle.putString("id", design.id)
// id だけでは Custom / StyleUri / 定義済みの区別がつかず、StyleUri(URI そのもの)が
// mapbox://styles/mapbox/<uri> の Custom として戻ってしまう。
val kind =
when (design) {
is MapboxMapDesign.StyleUri -> KIND_STYLE_URI
is MapboxMapDesign.Custom -> KIND_CUSTOM
is MapboxMapDesign -> if (design === MapboxMapDesign.create(design.id)) KIND_PRESET else KIND_VALUE
else -> KIND_VALUE
}
bundle.putString(KEY_KIND, kind)
bundle.putString(KEY_VALUE, design.getValue())
MapViewStateBundle.putAttributionRules(bundle, design.attributionRules)
}

override fun createState(
Expand All @@ -65,16 +82,42 @@ class MapboxMapViewSaver : BaseMapViewSaver<MapboxViewState>() {
): MapboxViewState =
MapboxViewState(
id = stateId,
mapDesignType =
MapboxMapDesign.create(
layerId = mapDesignBundle?.getString("id") ?: Standard.id,
),
mapDesignType = restoreDesign(mapDesignBundle),
cameraPosition = cameraPosition,
)

private fun restoreDesign(bundle: Bundle?): MapboxMapDesign {
val id = bundle?.getString("id") ?: return Standard
val rules = MapViewStateBundle.getAttributionRules(bundle)
return when (bundle.getString(KEY_KIND)) {
KIND_STYLE_URI -> MapboxMapDesign.StyleUri(uri = id, attributionRules = rules)
KIND_CUSTOM -> MapboxMapDesign.Custom(layerId = id, attributionRules = rules)
KIND_VALUE -> {
// アプリ独自の実装。地図に渡していた値(スタイル URI)から作り直す。
val value = bundle.getString(KEY_VALUE) ?: id
val prefix = "${MapboxMapDesign.MAPBOX_URL}/"
if (value.startsWith(prefix)) {
MapboxMapDesign.Custom(layerId = value.removePrefix(prefix), attributionRules = rules)
} else {
MapboxMapDesign.StyleUri(uri = value, attributionRules = rules)
}
}
// KIND_PRESET と、kind を持たない以前の形式
else -> MapboxMapDesign.create(layerId = id)
}
}

override fun getStateId(state: MapboxViewState): String = state.id
}

// 公開 API の門番(javap)にコンパニオンの定数が載らないよう、ファイル private にする。
private const val KEY_KIND = "kind"
private const val KEY_VALUE = "value"
private const val KIND_PRESET = "preset"
private const val KIND_CUSTOM = "custom"
private const val KIND_STYLE_URI = "styleUri"
private const val KIND_VALUE = "value"

@Composable
fun rememberMapboxMapViewState(
mapDesign: MapboxDesignType = Standard,
Expand Down
Loading