diff --git a/src/androidTest/java/com/mapconductor/mapbox/MapboxMapViewSaverTest.kt b/src/androidTest/java/com/mapconductor/mapbox/MapboxMapViewSaverTest.kt new file mode 100644 index 0000000..471cb44 --- /dev/null +++ b/src/androidTest/java/com/mapconductor/mapbox/MapboxMapViewSaverTest.kt @@ -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/ の + // 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 { + override val id: String = "app-design" + override val attributionRules: List = 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 roundTrip( + saver: Saver, + 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() + } + } +} diff --git a/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt b/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt index b2ab6c7..1e0adea 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt @@ -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 @@ -39,6 +40,9 @@ class MapboxViewState( } internal fun onMapDesignTypeChange(value: MapboxDesignType) { + // 地図から読み戻した値はスタイル URI から作ったデザインで、attributionRules を持たない。 + // 同じスタイルなら、アプリが渡した(あるいは復元した)デザインを残す。 + if (value.getValue() == _mapDesignType.getValue()) return _mapDesignType = value } @@ -55,7 +59,20 @@ class MapboxMapViewSaver : BaseMapViewSaver() { 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/ の 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( @@ -65,16 +82,42 @@ class MapboxMapViewSaver : BaseMapViewSaver() { ): 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,