diff --git a/build.gradle.kts b/build.gradle.kts index 3a2abb2..8fbd89f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -89,6 +89,11 @@ dependencies { } else { api("com.mapconductor:compose:$libraryVersion") } + + testImplementation(libs.junit) + // Saver の保存 → 復元を実機の Bundle / Parcel で確かめる(SaverRoundTripTest)。 + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) } // Set project version for NMCP plugin diff --git a/src/androidTest/java/com/mapconductor/maptiler/MapTilerMapViewSaverTest.kt b/src/androidTest/java/com/mapconductor/maptiler/MapTilerMapViewSaverTest.kt new file mode 100644 index 0000000..4a01cb1 --- /dev/null +++ b/src/androidTest/java/com/mapconductor/maptiler/MapTilerMapViewSaverTest.kt @@ -0,0 +1,102 @@ +package com.mapconductor.maptiler + +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.MapUISettings +import com.maptiler.maptilersdk.map.style.MTMapReferenceStyle +import com.maptiler.maptilersdk.map.style.MTMapStyleVariant +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import java.net.URL +import android.os.Bundle +import android.os.Parcel + +/** + * 回転などの構成変更で state が [MapTilerMapViewSaver] を通って作り直されても、デザイン(id・ + * attributionRules ほか)と uiSettings・カメラが落ちないこと。Bundle は実機の Parcel を一度通す。 + */ +@RunWith(AndroidJUnit4::class) +class MapTilerMapViewSaverTest { + 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 keepsCustomStyleAndAttributionRules() { + // 以前は id だけを保存して fromId で戻しており、定義に無い id は Streets になっていた。 + val design = + MapTilerDesign( + id = "my-style", + referenceStyle = MTMapReferenceStyle.CUSTOM(URL("https://example.com/style.json")), + attributionRules = rules, + ) + val state = MapTilerViewState(mapDesignType = design, id = "maptiler-1", initialCameraPosition = camera) + state.uiSettings = MapUISettings(rotateGesture = false, tiltGesture = false) + + val restored = roundTrip(MapTilerMapViewSaver().createSaver(), state) + + val actual = restored.mapDesignType + assertEquals("my-style", actual.id) + val style = actual.referenceStyle + assertTrue(style is MTMapReferenceStyle.CUSTOM) + assertEquals("https://example.com/style.json", (style as MTMapReferenceStyle.CUSTOM).url.toString()) + assertEquals(null, actual.variant) + assertEquals(rules, actual.attributionRules) + assertCommon(restored, "maptiler-1") + } + + @Test + fun keepsReferenceStyleAndVariant() { + val design = MapTilerDesign("outdoor-dark", MTMapReferenceStyle.OUTDOOR, MTMapStyleVariant.DARK) + val restored = roundTrip(MapTilerMapViewSaver().createSaver(), MapTilerViewState(design, "maptiler-2")) + assertEquals(design, restored.mapDesignType) + } + + @Test + fun keepsPresetDesign() { + val restored = roundTrip(MapTilerMapViewSaver().createSaver(), MapTilerViewState(MapTilerDesign.StreetsDark, "maptiler-3")) + assertEquals(MapTilerDesign.StreetsDark, restored.mapDesignType) + } + + 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/maptiler/MapTilerViewStateImpl.kt b/src/main/java/com/mapconductor/maptiler/MapTilerViewStateImpl.kt index 84072aa..3d3b926 100644 --- a/src/main/java/com/mapconductor/maptiler/MapTilerViewStateImpl.kt +++ b/src/main/java/com/mapconductor/maptiler/MapTilerViewStateImpl.kt @@ -9,6 +9,10 @@ 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.maptiler.maptilersdk.map.style.MTMapReferenceStyle +import com.maptiler.maptilersdk.map.style.MTMapStyleVariant +import java.net.URL import android.os.Bundle /** @@ -68,20 +72,52 @@ class MapTilerMapViewSaver : BaseMapViewSaver() { state: MapTilerViewState, bundle: Bundle, ) { - bundle.putString(KEY_DESIGN_ID, state.mapDesignType.id) + val design = state.mapDesignType + bundle.putString(KEY_DESIGN_ID, design.id) + when (val style = design.referenceStyle) { + is MTMapReferenceStyle.CUSTOM -> bundle.putString(KEY_CUSTOM_STYLE_URL, style.url.toString()) + else -> bundle.putString(KEY_REFERENCE_STYLE, style.getName()) + } + design.variant?.let { bundle.putString(KEY_VARIANT, it.name) } + MapViewStateBundle.putAttributionRules(bundle, design.attributionRules) } override fun createState( stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): MapTilerViewState { - val designId = mapDesignBundle?.getString(KEY_DESIGN_ID) - return MapTilerViewState( - mapDesignType = MapTilerDesign.fromId(designId), + ): MapTilerViewState = + MapTilerViewState( + mapDesignType = restoreDesign(mapDesignBundle), id = stateId, initialCameraPosition = cameraPosition, ) + + /** + * id だけで [MapTilerDesign.fromId] に頼ると、定義に無いデザイン(独自のスタイル URL や + * バリアントの組み合わせ)は Streets に化け、attributionRules も落ちる。 + * スタイルとバリアントも保存して作り直す。 + */ + private fun restoreDesign(bundle: Bundle?): MapTilerDesign { + val designId = bundle?.getString(KEY_DESIGN_ID) + val referenceStyle = + bundle?.getString(KEY_CUSTOM_STYLE_URL)?.let { url -> + runCatching { MTMapReferenceStyle.CUSTOM(URL(url)) }.getOrNull() + } ?: bundle?.getString(KEY_REFERENCE_STYLE)?.let { name -> + MTMapReferenceStyle.all().firstOrNull { it.getName() == name } + } + // スタイルを持たない以前の形式は id から引く。 + if (designId == null || referenceStyle == null) return MapTilerDesign.fromId(designId) + val variant = + bundle.getString(KEY_VARIANT)?.let { name -> + MTMapStyleVariant.values().firstOrNull { it.name == name } + } + return MapTilerDesign( + id = designId, + referenceStyle = referenceStyle, + variant = variant, + attributionRules = MapViewStateBundle.getAttributionRules(bundle), + ) } override fun getStateId(state: MapTilerViewState): String = state.id @@ -91,6 +127,11 @@ class MapTilerMapViewSaver : BaseMapViewSaver() { } } +// 公開 API の門番(javap)にコンパニオンの定数が載らないよう、ファイル private にする。 +private const val KEY_REFERENCE_STYLE = "maptiler_reference_style" +private const val KEY_CUSTOM_STYLE_URL = "maptiler_custom_style_url" +private const val KEY_VARIANT = "maptiler_variant" + /** * [MapTilerViewState] を生成・記憶する Composable ファクトリ。 *