diff --git a/src/androidTest/java/com/mapconductor/tomtom/TomTomMapViewSaverTest.kt b/src/androidTest/java/com/mapconductor/tomtom/TomTomMapViewSaverTest.kt new file mode 100644 index 0000000..3b8e74f --- /dev/null +++ b/src/androidTest/java/com/mapconductor/tomtom/TomTomMapViewSaverTest.kt @@ -0,0 +1,87 @@ +package com.mapconductor.tomtom + +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.tomtom.sdk.map.display.style.StyleDescriptor +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.net.Uri +import android.os.Bundle +import android.os.Parcel + +/** + * 回転などの構成変更で state が [TomTomMapViewSaver] を通って作り直されても、デザイン(id・ + * attributionRules ほか)と uiSettings・カメラが落ちないこと。Bundle は実機の Parcel を一度通す。 + */ +@RunWith(AndroidJUnit4::class) +class TomTomMapViewSaverTest { + 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 keepsCustomDesignWithItsStyleDescriptor() { + val descriptor = StyleDescriptor(Uri.parse("https://example.com/style.json")) + val design = TomTomMapDesign.Custom("my-style", descriptor, rules) + val state = TomTomMapViewState(id = "tomtom-1", mapDesignType = design, cameraPosition = camera) + state.uiSettings = MapUISettings(rotateGesture = false, tiltGesture = false) + + val restored = roundTrip(TomTomMapViewSaver().createSaver(), state) + + // 以前は id だけを保存して create(id) で戻しており、Standard になっていた。 + val actual = restored.mapDesignType + assertTrue(actual is TomTomMapDesign.Custom) + actual as TomTomMapDesign.Custom + assertEquals("my-style", actual.id) + assertEquals(descriptor, actual.styleDescriptor) + assertEquals(rules, actual.attributionRules) + assertCommon(restored, "tomtom-1") + } + + @Test + fun keepsPresetDesignAsTheSameObject() { + val state = TomTomMapViewState(id = "tomtom-2", mapDesignType = TomTomMapDesign.Satellite) + assertSame(TomTomMapDesign.Satellite, roundTrip(TomTomMapViewSaver().createSaver(), state).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/tomtom/TomTomMapView.kt b/src/main/java/com/mapconductor/tomtom/TomTomMapView.kt index cd55210..29f4a23 100644 --- a/src/main/java/com/mapconductor/tomtom/TomTomMapView.kt +++ b/src/main/java/com/mapconductor/tomtom/TomTomMapView.kt @@ -157,7 +157,6 @@ fun TomTomMapView( } mapController.setMapClickListener(onMapClick) mapController.setMapLongClickListener(onMapLongClick) - mapController.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) cameraRestriction?.let { mapController.setCameraRestriction(it) } // 大量マーカーをラスタタイル化するため、マーカータイル用ラスタレイヤーの // 実体化(スタイル合成 + loadStyle)を配線する。 @@ -165,6 +164,17 @@ fun TomTomMapView( apiKey = tomtomApiKey(context), cacheDir = context.cacheDir, ) + // 地図は MapOptions の既定スタイル(= Standard)で作られる。state のデザインが + // それ以外なら載せる。そのあとでリスナーを繋ぐ: 先に繋ぐとコントローラの既定値 + // (Standard)が state へ返り、アプリが渡した(あるいは回転後に復元した) + // デザインが Standard に化ける。 + val initialDesign = state.mapDesignType + if (initialDesign === TomTomMapDesign.Standard) { + mapController.mapDesignType = initialDesign + } else { + mapController.setMapDesignType(initialDesign) + } + mapController.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) holder.mapView.post { // Style loading can reset the camera, so reapply it once the view is attached. mapController.moveCamera(initialCameraPosition) diff --git a/src/main/java/com/mapconductor/tomtom/TomTomMapViewStateImpl.kt b/src/main/java/com/mapconductor/tomtom/TomTomMapViewStateImpl.kt index 8192ec2..6609e88 100644 --- a/src/main/java/com/mapconductor/tomtom/TomTomMapViewStateImpl.kt +++ b/src/main/java/com/mapconductor/tomtom/TomTomMapViewStateImpl.kt @@ -9,7 +9,9 @@ import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapCameraPositionInterface import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.MapViewStateBundle import com.mapconductor.core.map.MapViewStateInterface +import com.tomtom.sdk.map.display.style.StyleDescriptor import java.util.UUID import android.os.Bundle import kotlinx.coroutines.flow.MutableStateFlow @@ -61,7 +63,14 @@ class TomTomMapViewSaver : BaseMapViewSaver() { state: TomTomMapViewState, bundle: Bundle, ) { - bundle.putString("id", state.mapDesignType.id) + val design = state.mapDesignType + bundle.putString("id", design.id) + // 定義済み(Standard など)以外は、地図に読み込ませる StyleDescriptor(Parcelable)ごと + // 保存して Custom で戻す。id だけだと Custom は Standard に化け、attributionRules も落ちる。 + if (design is TomTomMapDesign && design !== TomTomMapDesign.create(design.id)) { + bundle.putParcelable(KEY_STYLE_DESCRIPTOR, design.styleDescriptor) + } + MapViewStateBundle.putAttributionRules(bundle, design.attributionRules) } override fun createState( @@ -71,16 +80,30 @@ class TomTomMapViewSaver : BaseMapViewSaver() { ): TomTomMapViewState = TomTomMapViewState( id = stateId, - mapDesignType = - TomTomMapDesign.create( - id = mapDesignBundle?.getString("id") ?: TomTomMapDesign.Standard.id, - ), + mapDesignType = restoreDesign(mapDesignBundle), cameraPosition = cameraPosition, ) + private fun restoreDesign(bundle: Bundle?): TomTomMapDesign { + val id = bundle?.getString("id") ?: return TomTomMapDesign.Standard + val rules = MapViewStateBundle.getAttributionRules(bundle) + + @Suppress("DEPRECATION") + val descriptor = bundle.getParcelable(KEY_STYLE_DESCRIPTOR) + val preset = TomTomMapDesign.create(id) + return when { + descriptor != null -> TomTomMapDesign.Custom(id, descriptor, rules) + rules.isNotEmpty() -> TomTomMapDesign.Custom(id, preset.styleDescriptor, rules) + else -> preset + } + } + override fun getStateId(state: TomTomMapViewState): String = state.id } +// 公開 API の門番(javap)にコンパニオンの定数が載らないよう、ファイル private にする。 +private const val KEY_STYLE_DESCRIPTOR = "styleDescriptor" + @Composable fun rememberTomTomMapViewState( mapDesign: TomTomMapDesign = TomTomMapDesign.Standard,