From 76268968b3242081196a7a305f61414267ab2f32 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 6 Nov 2026 19:25:13 -0800 Subject: [PATCH] Keep the map design across configuration changes The saver kept only the id and restored through fromId, so custom styles or variants became Streets and attribution rules were lost. It now saves the reference style (or custom URL), variant and rules. Adds a device round-trip test. --- build.gradle.kts | 5 + .../maptiler/MapTilerMapViewSaverTest.kt | 102 ++++++++++++++++++ .../maptiler/MapTilerViewStateImpl.kt | 51 ++++++++- 3 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 src/androidTest/java/com/mapconductor/maptiler/MapTilerMapViewSaverTest.kt 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 ファクトリ。 *