diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index e60eb65a8df..1aa084c3c94 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -16,6 +16,9 @@ android { targetSdk = 37 versionCode = 6308 versionName = "64.0-alpha1" + + // Match the ABIs supported by the MapLibre Compose runtime. + ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64") } } signingConfigs { @@ -68,7 +71,6 @@ dependencies { implementation("androidx.activity:activity-compose:1.13.0") implementation("com.google.android.material:material:1.14.0") implementation("androidx.appcompat:appcompat:1.7.1") - implementation("androidx.fragment:fragment-ktx:1.8.6") // Compose implementation("org.jetbrains.compose.runtime:runtime:1.12.0") @@ -82,7 +84,7 @@ dependencies { // Dependency Injection implementation("io.insert-koin:koin-android:4.2.2") - implementation("io.insert-koin:koin-compose:4.2.2") + implementation("io.insert-koin:koin-androidx-compose:4.2.2") implementation("io.insert-koin:koin-androidx-workmanager:4.2.2") // Settings diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index b81dead3329..e7a3a76e0e2 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -22,8 +22,8 @@ - - + + diff --git a/androidApp/src/main/kotlin/de/westnordost/streetcomplete/screens/main/MainActivity.kt b/androidApp/src/main/kotlin/de/westnordost/streetcomplete/screens/main/MainActivity.kt index 56fea39732c..f95b204d30e 100644 --- a/androidApp/src/main/kotlin/de/westnordost/streetcomplete/screens/main/MainActivity.kt +++ b/androidApp/src/main/kotlin/de/westnordost/streetcomplete/screens/main/MainActivity.kt @@ -1,376 +1,76 @@ package de.westnordost.streetcomplete.screens.main import android.content.Intent -import android.graphics.PointF import android.os.Bundle -import android.view.View -import android.view.ViewGroup import android.view.WindowManager -import android.widget.RelativeLayout -import android.widget.Toast import androidx.activity.enableEdgeToEdge -import androidx.annotation.AnyThread -import androidx.annotation.UiThread -import androidx.appcompat.app.AlertDialog -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalWindowInfo -import androidx.compose.ui.platform.WindowInfo -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.LayoutDirection -import androidx.fragment.app.FragmentContainerView -import androidx.fragment.app.commit -import androidx.lifecycle.lifecycleScope -import de.westnordost.osmfeatures.FeatureDictionary -import de.westnordost.streetcomplete.ApplicationConstants -import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.data.FeedsUpdater import de.westnordost.streetcomplete.data.PeriodicCleaner -import de.westnordost.streetcomplete.data.download.tiles.asBoundingBoxOfEnclosingTiles -import de.westnordost.streetcomplete.data.edithistory.EditKey -import de.westnordost.streetcomplete.data.osm.edits.MapDataWithEditsSource -import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox -import de.westnordost.streetcomplete.data.osm.mapdata.Element -import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.data.osm.mapdata.LazyMapDataWithGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.key -import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuest -import de.westnordost.streetcomplete.data.osmnotes.edits.NotesWithEditsSource -import de.westnordost.streetcomplete.data.osmtracks.Trackpoint -import de.westnordost.streetcomplete.data.overlays.Overlay import de.westnordost.streetcomplete.data.preferences.Preferences import de.westnordost.streetcomplete.data.quest.AutoSyncer -import de.westnordost.streetcomplete.data.quest.Quest -import de.westnordost.streetcomplete.data.quest.QuestKey -import de.westnordost.streetcomplete.data.quest.VisibleQuestsSource -import de.westnordost.streetcomplete.data.visiblequests.QuestsHiddenSource -import de.westnordost.streetcomplete.osm.level.levelsIntersect -import de.westnordost.streetcomplete.osm.level.parseLevelsOrNull import de.westnordost.streetcomplete.screens.BaseActivity import de.westnordost.streetcomplete.screens.about.AboutActivity -import de.westnordost.streetcomplete.screens.main.controls.LocationState -import de.westnordost.streetcomplete.screens.main.edithistory.EditHistoryViewModel -import de.westnordost.streetcomplete.screens.main.edithistory.icon -import de.westnordost.streetcomplete.screens.main.map.MainMapFragment -import de.westnordost.streetcomplete.screens.main.map.MapFragment -import de.westnordost.streetcomplete.screens.main.map.getIcon -import de.westnordost.streetcomplete.screens.main.map.getTitle -import de.westnordost.streetcomplete.screens.main.map.maplibre.CameraPosition -import de.westnordost.streetcomplete.screens.main.map.maplibre.Padding -import de.westnordost.streetcomplete.screens.main.map.maplibre.toPadding import de.westnordost.streetcomplete.screens.settings.SettingsActivity import de.westnordost.streetcomplete.screens.user.UserActivity -import de.westnordost.streetcomplete.ui.common.quest.MapClick -import de.westnordost.streetcomplete.ui.common.quest.Marker -import de.westnordost.streetcomplete.ui.ktx.toDpOffset import de.westnordost.streetcomplete.ui.theme.AppTheme -import de.westnordost.streetcomplete.ui.theme.Dimensions -import de.westnordost.streetcomplete.util.ktx.getLocationInWindow -import de.westnordost.streetcomplete.util.ktx.observe -import de.westnordost.streetcomplete.util.ktx.toLatLon -import de.westnordost.streetcomplete.util.ktx.toOffset -import de.westnordost.streetcomplete.util.ktx.toast -import de.westnordost.streetcomplete.util.ktx.updatesWithPermissionChanges -import de.westnordost.streetcomplete.util.math.area -import de.westnordost.streetcomplete.util.math.enclosingBoundingBox -import de.westnordost.streetcomplete.util.math.enlargedBy -import de.westnordost.streetcomplete.view.toAndroidResourceId -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject import org.koin.android.scope.AndroidScopeComponent +import org.koin.androidx.compose.scope.KoinActivityScope import org.koin.androidx.scope.activityScope -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.qualifier.named import org.koin.core.scope.Scope -import org.maplibre.compose.location.LocationEvent -import org.maplibre.compose.location.LocationPermission -import org.maplibre.compose.location.LocationProvider -import org.maplibre.compose.location.LocationRequest -import org.maplibre.compose.location.LocationUnavailableReason -import org.maplibre.compose.location.SystemSettingsLauncher -import kotlin.math.PI -import kotlin.math.sqrt - -/** Controls the main view. - * - * The logical sub components of this main view are all outsourced into individual child fragments - * with which this fragment communicates with. - * - * The child fragments do not communicate with each other but only with their parent (this class) - * and the parent then controls its children. Hence, all the logic when interacting with the - * map / bottom sheets / sidebars / buttons etc. passes through this class and this is why this - * class implements all the listeners of its child fragments. - * - * This class does not contain so much logic itself, it delegates most of it to its children. - * Think of it as the wiring that binds all the components together. - * - * Still, as this is by far the largest in terms of lines of code. For easier reading, in - * IntelliJ, you can collapse sections of this class that start with "//region" using the little - * [-] icon next to it. - * - */ -class MainActivity : - BaseActivity(), - // listeners to child fragments: - MapFragment.Listener, - MainMapFragment.Listener, - // listeners to changes to data: - VisibleQuestsSource.Listener, - MapDataWithEditsSource.Listener, - // rest - AndroidScopeComponent { +/** Android host for the shared main screen and application lifecycle work. */ +class MainActivity : BaseActivity(), AndroidScopeComponent { override val scope: Scope by activityScope() private val autoSyncer: AutoSyncer by inject() private val prefs: Preferences by inject() - private val visibleQuestsSource: VisibleQuestsSource by inject() - private val mapDataWithEditsSource: MapDataWithEditsSource by inject() - private val notesSource: NotesWithEditsSource by inject() - private val questsHiddenSource: QuestsHiddenSource by inject() private val feedsUpdater: FeedsUpdater by inject() - private val featureDictionary: Lazy by inject(named("FeatureDictionaryLazy")) - private val locationProvider: LocationProvider by inject() - private val systemSettingsLauncher: SystemSettingsLauncher by inject() private val periodicCleaner: PeriodicCleaner by inject() - - private val viewModel by viewModel() - private val editHistoryViewModel by viewModel() - private val mainBottomSheetViewModel by viewModel() - - private val showMapContextMenu = mutableStateOf(false) - private val lastMapLongPress = mutableStateOf?>(null) - - private val lastMapClick = mutableStateOf(null) - - private var windowInfo: WindowInfo? = null - - // for freezing the map while sidebar is open - private var wasFollowingPosition: Boolean? = null - private var wasNavigationMode: Boolean? = null - - private val mapFragment: MainMapFragment? get() = - supportFragmentManager.findFragmentByTag(TAG_MAP) as MainMapFragment? - - /* +++++++++++++++++++++++++++++++++++++++ CALLBACKS ++++++++++++++++++++++++++++++++++++++++ */ - - //region Lifecycle - Android Lifecycle Callbacks + private val uri = mutableStateOf(null) override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() + // Drop legacy fragments before AndroidX restores their removed classes, retaining Compose state. + savedInstanceState?.getBundle("androidx.lifecycle.BundlableSavedStateRegistry.key") + ?.remove("android:support:fragments") super.onCreate(savedInstanceState) - val root = RelativeLayout(this) - val compose = ComposeView(this) - val mapContainer = FragmentContainerView(this).also { it.id = 1 } - root.addView(mapContainer, ViewGroup.LayoutParams(-1, -1)) - root.addView(compose, ViewGroup.LayoutParams(-1, -1)) - - setContentView(root) - - if (savedInstanceState == null) { - handleIntent(intent) - - supportFragmentManager.commit { - add(mapContainer, MainMapFragment(), TAG_MAP) - } - } - + if (savedInstanceState == null) handleIntent(intent) lifecycle.addObserver(autoSyncer) - feedsUpdater.updateAtMostDaily() - // this must be enqueued once the UI is started, i.e. not in headless mode. This is why - // it is done here, rather than in AppInitializer. Reason is that - // AppInitializer.initialize() is also executed when a background job is run. But we don't - // want to enqueue the cleanup job again while running the cleanup job, but only once after - // the user actually opened the app! + // Only enqueue cleanup when the UI starts, not when AppInitializer runs a background job. periodicCleaner.enqueue() - compose.setContent { AppTheme { - val mapAppLauncher = rememberMapAppLauncher() - var lastQuestSolved by remember { mutableStateOf(null) } - - windowInfo = LocalWindowInfo.current - val context = LocalContext.current - - MainScreen( - viewModel = viewModel, - editHistoryViewModel = editHistoryViewModel, - mainBottomSheetViewModel = mainBottomSheetViewModel, - onClickZoomIn = ::onClickZoomIn, - onClickZoomOut = ::onClickZoomOut, - onZoomDrag = ::onZoomDrag, - onClickCompass = ::onClickCompassButton, - onClickLocation = ::onClickLocationButton, - onClickLocationPointer = ::onClickLocationPointer, - onClickCreate = ::onClickCreateButton, - onClickStopTrackRecording = ::onClickTracksStop, - onDownload = ::onClickDownload, - onClickSettings = { - context.startActivity(Intent(context, SettingsActivity::class.java)) - }, - onClickQuestSettings = { - context.startActivity(SettingsActivity.createLaunchQuestSettingsIntent(context)) - }, - onClickAbout = { - context.startActivity(Intent(context, AboutActivity::class.java)) - }, - onClickProfile = { - context.startActivity(Intent(context, UserActivity::class.java)) - }, - onClickLogin = { - val intent = Intent(context, UserActivity::class.java) - intent.putExtra(UserActivity.EXTRA_LAUNCH_AUTH, true) - context.startActivity(intent) - }, - onSetMapMarkers = { markers -> - mapFragment?.setMarkersForCurrentHighlighting(markers) - }, - onSolvedQuest = { icon, position -> - val offset = root.getLocationInWindow() - val startPos = mapFragment?.getPointOf(position)!! - - startPos.x += offset.x - startPos.y += offset.y - - lastQuestSolved = QuestSolvedEvent(icon, startPos.toOffset()) - }, - getOffset = { position -> - val offset = root.getLocationInWindow() - val position = mapFragment?.getPointOf(position)!! - position.x += offset.x - position.y += offset.y - position.toOffset() - }, - lastMapClick = lastMapClick.value, - ) - - lastQuestSolved?.let { LastQuestSolvedEffect(it) } - - val lastLongPressOffset = lastMapLongPress.value?.first ?: Offset.Zero - val lastLongPressPosition = lastMapLongPress.value?.second - MapContextMenu( - expanded = showMapContextMenu.value, - onDismissRequest = { showMapContextMenu.value = false }, - onClickCreateNote = { lastLongPressPosition?.let { onClickCreateNote(it) } }, - onClickCreateTrack = { onClickCreateTrack() }, - isOpenLocationAvailable = mapAppLauncher.isAvailable(), - onClickOpenLocation = { - if (lastLongPressPosition != null) { - mapAppLauncher.openAt( - position = lastLongPressPosition, - zoom = mapFragment?.cameraPosition?.zoom ?: 18.0 + setContentView(ComposeView(this).apply { + setContent { + AppTheme { + KoinActivityScope { + MainScreen( + uri = uri.value, + onConsumedUri = { uri.value = null }, + onClickSettings = { startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) }, + onClickQuestSettings = { startActivity(SettingsActivity.createLaunchQuestSettingsIntent(this@MainActivity)) }, + onClickAbout = { startActivity(Intent(this@MainActivity, AboutActivity::class.java)) }, + onClickProfile = { startActivity(Intent(this@MainActivity, UserActivity::class.java)) }, + onClickLogin = { + startActivity(Intent(this@MainActivity, UserActivity::class.java).apply { + putExtra(UserActivity.EXTRA_LAUNCH_AUTH, true) + }) + }, ) } - }, - offset = lastLongPressOffset.toDpOffset() - ) - } } - - observe(editHistoryViewModel.selectedEdit) { edit -> - if (edit != null) { - val geometry = editHistoryViewModel.getEditGeometry(edit) - mapFragment?.startFocus(geometry, null) - mapFragment?.highlightGeometry(geometry) - mapFragment?.highlightPins(edit.icon!!.toAndroidResourceId()!!, listOf(edit.position)) - mapFragment?.hideOverlay() - } else if (editHistoryViewModel.isShowingSidebar.value) { - mapFragment?.clearFocus() - mapFragment?.clearHighlighting() - mapFragment?.hideOverlay() // because clearHighlighting shows overlay again :-/ - } - } - observe(editHistoryViewModel.isShowingSidebar) { isShowingSidebar -> - if (!isShowingSidebar) { - unfreezeMap() - mapFragment?.clearFocus() - mapFragment?.clearHighlighting() - mapFragment?.pinMode = MainMapFragment.PinMode.QUESTS - } else { - freezeMap() - mapFragment?.hideOverlay() - mapFragment?.pinMode = MainMapFragment.PinMode.EDITS - } - } - observe(viewModel.geoUri) { geoUri -> - if (geoUri != null) { - viewModel.consumeGeoUri() - mapFragment?.setInitialCameraPosition(geoUri) - viewModel.isFollowingPosition.value = mapFragment?.isFollowingPosition ?: false - viewModel.isNavigationMode.value = mapFragment?.isNavigationMode ?: false - } - } - observe(mainBottomSheetViewModel.shownBottomSheet) { shownBottomSheet -> - updateBottomSheetElementPosition() - if (shownBottomSheet != null) { - freezeMap() - when (shownBottomSheet) { - is ShownBottomSheet.CreateOsmNote -> { - /* nothing more */ - } - is ShownBottomSheet.OsmNoteQuest -> { - showQuestDetailsOnMap(shownBottomSheet.quest, null) - } - is ShownBottomSheet.OsmQuest -> { - val element = shownBottomSheet.element - showQuestDetailsOnMap(shownBottomSheet.quest, element) - } - is ShownBottomSheet.Overlay -> { - val element = shownBottomSheet.element - if (element != null) { - showOverlayElementDetailsOnMap( - overlay = shownBottomSheet.overlay, - element = element, - geometry = shownBottomSheet.geometry!! - ) - } else { - showOverlayForNewElementOnMap(shownBottomSheet.overlay) - } - } } - } else { - clearHighlighting() - unfreezeMap() - mapFragment?.endFocus() } - } - observe(viewModel.selectedOverlay) { selectedOverlay -> - if (mainBottomSheetViewModel.shownBottomSheet.value is ShownBottomSheet.Overlay) { - mainBottomSheetViewModel.closeBottomSheet() - } - } - observe(locationProvider.updatesWithPermissionChanges(LocationRequest())) { locationEvent -> - viewModel.locationState.value = when (locationEvent) { - is LocationEvent.Update -> LocationState.UPDATING - is LocationEvent.Unavailable -> when (locationEvent.reason) { - LocationUnavailableReason.ServicesDisabled -> LocationState.ALLOWED - LocationUnavailableReason.TemporarilyUnavailable -> LocationState.SEARCHING - LocationUnavailableReason.PermissionDenied -> LocationState.DENIED - LocationUnavailableReason.Unsupported, - LocationUnavailableReason.UnexpectedFailure -> null - } - } - mapFragment?.onLocationEvent(locationEvent) - } + }) } override fun onStart() { super.onStart() - - updateScreenOn() - - visibleQuestsSource.addListener(this) - mapDataWithEditsSource.addListener(this) + if (prefs.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } override fun onNewIntent(intent: Intent) { @@ -379,490 +79,6 @@ class MainActivity : } private fun handleIntent(intent: Intent) { - if (intent.action != Intent.ACTION_VIEW) return - val data = intent.data?.toString() ?: return - viewModel.setUri(data) - } - - override fun onStop() { - super.onStop() - - visibleQuestsSource.removeListener(this) - mapDataWithEditsSource.removeListener(this) - } - - //endregion - - /* ------------------------------- Preferences listeners ------------------------------------ */ - - private fun updateScreenOn() { - if (prefs.keepScreenOn) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - } - - //region QuestsMapFragment - Callbacks from the map with its quest pins - - /* ---------------------------------- MapFragment.Listener ---------------------------------- */ - - override fun onMapInitialized() { - viewModel.geoUri.value?.let { geoUri -> - viewModel.consumeGeoUri() - mapFragment?.setInitialCameraPosition(geoUri) - } - viewModel.isFollowingPosition.value = mapFragment?.isFollowingPosition ?: false - viewModel.isNavigationMode.value = mapFragment?.isNavigationMode ?: false - viewModel.isRecordingTracks.value = mapFragment?.isRecordingTracks ?: false - viewModel.mapCamera.value = mapFragment?.cameraPosition - viewModel.metersPerDp.value = mapFragment?.getMetersPerPixel() ?: 0.0 - updateBottomSheetElementPosition() - updateDisplayedPosition() - } - - override fun onMapIsChanging(camera: CameraPosition) { - viewModel.mapCamera.value = camera - viewModel.metersPerDp.value = mapFragment?.getMetersPerPixel() ?: 0.0 - updateBottomSheetElementPosition() - updateDisplayedPosition() - } - - override fun onPanBegin() { - /* panning only results in not following location anymore if a location is already known - and displayed - */ - if (mapFragment?.displayedLocation != null) { - setIsFollowingPosition(false) - } - } - - override fun onUserCameraMoveStarted() { - viewModel.userHasMovedCamera.value = true - } - - override fun onLongPress(point: PointF, position: LatLon) { - if (mainBottomSheetViewModel.shownBottomSheet.value != null || editHistoryViewModel.isShowingSidebar.value) return - - lastMapLongPress.value = Pair(Offset(point.x, point.y), position) - showMapContextMenu.value = true - } - - /* ---------------------------- MainMapFragment.Listener --------------------------- */ - - override fun onClickedQuest(questKey: QuestKey) { - if (mainBottomSheetViewModel.shownBottomSheet.value != null) return - mainBottomSheetViewModel.showQuest(questKey) - } - - override fun onClickedEdit(editKey: EditKey) { - editHistoryViewModel.select(editKey) - } - - override fun onClickedMapAt(position: LatLon, clickAreaSizeInMeters: Double) { - if (mainBottomSheetViewModel.shownBottomSheet.value != null) { - lastMapClick.value = MapClick(position, clickAreaSizeInMeters) - } else if (editHistoryViewModel.isShowingSidebar.value) { - editHistoryViewModel.hideSidebar() - } - } - - override fun onClickedElement(elementKey: ElementKey) { - val overlay = viewModel.selectedOverlay.value ?: return - if (mainBottomSheetViewModel.shownBottomSheet.value != null) return - mainBottomSheetViewModel.showElementInOverlay(overlay, elementKey) - } - - override fun onDisplayedLocationDidChange() { - updateDisplayedPosition() - } - - private fun updateDisplayedPosition() { - viewModel.displayedPosition.value = getDisplayedPoint()?.toOffset() - } - - private fun updateBottomSheetElementPosition() { - val bottomSheetElementPosition = mainBottomSheetViewModel.shownBottomSheet.value?.position - mainBottomSheetViewModel.geometryOffsetInWindow.value = - if (bottomSheetElementPosition != null) mapFragment?.getPointOf(bottomSheetElementPosition)?.toOffset() - else null - } - - private fun getDisplayedPoint(): PointF? { - val mapFragment = mapFragment ?: return null - val displayedPosition = mapFragment.displayedLocation?.position?.toLatLon() ?: return null - return mapFragment.getPointOf(displayedPosition) - } - - //endregion - - //region Data Updates - Callbacks for when data changed in the local database - - /* ---------------------------------- VisibleQuestListener ---------------------------------- */ - - @AnyThread - override fun onUpdated(added: Collection, removed: Collection) { - val questKey = - when (val shown = mainBottomSheetViewModel.shownBottomSheet.value) { - is ShownBottomSheet.OsmNoteQuest -> shown.quest.key - is ShownBottomSheet.OsmQuest -> shown.quest.key - else -> return - } - // open quest has been deleted - if (questKey in removed) { - mainBottomSheetViewModel.closeBottomSheet() - } - } - - @AnyThread - override fun onInvalidated() { - val questKey = - when (val shown = mainBottomSheetViewModel.shownBottomSheet.value) { - is ShownBottomSheet.OsmNoteQuest -> shown.quest.key - is ShownBottomSheet.OsmQuest -> shown.quest.key - else -> return - } - - lifecycleScope.launch { - val openQuest = withContext(Dispatchers.IO) { visibleQuestsSource.get(questKey) } - // open quest does not exist anymore after visible quest invalidation - if (openQuest == null) { - mainBottomSheetViewModel.closeBottomSheet() - } - } - } - - /* ---------------------------- MapDataWithEditsSource.Listener ----------------------------- */ - - @AnyThread - override fun onUpdated(updated: MapDataWithGeometry, deleted: Collection) { - val elementKey = (mainBottomSheetViewModel.shownBottomSheet.value as? ShownBottomSheet.Overlay)?.element?.key ?: return - if (elementKey in deleted) { - mainBottomSheetViewModel.closeBottomSheet() - } - } - - @AnyThread - override fun onReplacedForBBox(bbox: BoundingBox, mapDataWithGeometry: MapDataWithGeometry) { - val elementKey = (mainBottomSheetViewModel.shownBottomSheet.value as? ShownBottomSheet.Overlay)?.element?.key ?: return - lifecycleScope.launch { - val openElement = withContext(Dispatchers.IO) { mapDataWithEditsSource.get(elementKey.type, elementKey.id) } - // open element does not exist anymore after download - if (openElement == null) { - mainBottomSheetViewModel.closeBottomSheet() - } - } - } - - @AnyThread - override fun onCleared() { - val elementKey = (mainBottomSheetViewModel.shownBottomSheet.value as? ShownBottomSheet.Overlay)?.element?.key ?: return - mainBottomSheetViewModel.closeBottomSheet() - } - - //endregion - - /* ++++++++++++++++++++++++++++++++++++++ VIEW CONTROL ++++++++++++++++++++++++++++++++++++++ */ - - //region Buttons - Functionality for the buttons in the main view - - private fun onClickDownload() { - val downloadBbox = getDownloadArea() ?: return - viewModel.download(downloadBbox) - } - - private fun onClickZoomOut() { - mapFragment?.updateCameraPosition(300) { zoomBy = -1.0 } - } - - private fun onClickZoomIn() { - mapFragment?.updateCameraPosition(300) { zoomBy = +1.0 } - } - - private fun onZoomDrag(dp: Float) { - mapFragment?.updateCameraPosition(300) { zoomBy = dp / 20.0 } - } - - private fun onClickTracksStop() { - // hide the track information - viewModel.isRecordingTracks.value = false - val mapFragment = mapFragment ?: return - mapFragment.stopPositionTrackRecording() - val pos = mapFragment.displayedLocation?.position?.toLatLon() ?: return - composeNote(pos, mapFragment.recordedTracks.takeIf { it.isNotEmpty() }) - } - - private fun onClickCompassButton() { - // Clicking the compass button will always rotate the map back to north and remove tilt - val mapFragment = mapFragment ?: return - val camera = mapFragment.cameraPosition ?: return - - // if the user wants to rotate back north, it means he also doesn't want to use nav mode anymore - if (mapFragment.isNavigationMode) { - mapFragment.updateCameraPosition(300) { rotation = 0.0 } - setIsNavigationMode(false) - } else { - mapFragment.updateCameraPosition(300) { - rotation = 0.0 - tilt = 0.0 - } - } - } - - private fun onClickLocationButton() { - val mapFragment = mapFragment ?: return - - val permission = locationProvider.permission.value - if (permission is LocationPermission.NotGranted) { - if (permission.canRequest != false) { - if (!permission.shouldShowRationale) { - locationProvider.requestPermission() - } else { - AlertDialog.Builder(this) - .setTitle(R.string.no_location_permission_warning_title) - .setMessage(R.string.no_location_permission_warning) - .setPositiveButton(R.string.ok) { _, _ -> - locationProvider.requestPermission() - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - } else { - if (systemSettingsLauncher.canOpenApplicationSettings) { - AlertDialog.Builder(this) - .setMessage(R.string.turn_on_location_request) - .setPositiveButton(R.string.ok) { _, _ -> - systemSettingsLauncher.openApplicationSettings() - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } else { - toast(R.string.no_gps_no_quests) - } - } - } else { - when { - viewModel.locationState.value == LocationState.ALLOWED -> { - if (systemSettingsLauncher.canOpenLocationServicesSettings) { - AlertDialog.Builder(this) - .setMessage(R.string.turn_on_location_request) - .setPositiveButton(R.string.ok) { _, _ -> - systemSettingsLauncher.openLocationServicesSettings() - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } else { - toast(R.string.no_gps_no_quests) - } - } - !mapFragment.isFollowingPosition -> { - setIsFollowingPosition(true) - } - else -> { - setIsNavigationMode(!mapFragment.isNavigationMode) - } - } - } - } - - private fun onClickLocationPointer() { - setIsFollowingPosition(true) - } - - private fun onClickCreateButton() { - val overlay = viewModel.selectedOverlay.value ?: return - mainBottomSheetViewModel.showCreateElementInOverlay(overlay) - } - - private fun setIsNavigationMode(navigation: Boolean) { - mapFragment?.isNavigationMode = navigation - viewModel.isNavigationMode.value = navigation - } - - private fun setIsFollowingPosition(follow: Boolean) { - mapFragment?.isFollowingPosition = follow - viewModel.isFollowingPosition.value = follow - if (follow) mapFragment?.centerCurrentPositionIfFollowing() - } - - private fun getDownloadArea(): BoundingBox? { - val displayArea = mapFragment?.getDisplayedArea() - if (displayArea == null) { - toast(R.string.cannot_find_bbox_or_reduce_tilt, Toast.LENGTH_LONG) - return null - } - - val enclosingBBox = displayArea.asBoundingBoxOfEnclosingTiles(ApplicationConstants.DOWNLOAD_TILE_ZOOM) - val areaInSqKm = enclosingBBox.area() / 1000000 - if (areaInSqKm > ApplicationConstants.MAX_DOWNLOADABLE_AREA_IN_SQKM) { - toast(R.string.download_area_too_big, Toast.LENGTH_LONG) - return null - } - - // below a certain threshold, it does not make sense to download, so let's enlarge it - if (areaInSqKm < ApplicationConstants.MIN_DOWNLOADABLE_AREA_IN_SQKM) { - val cameraPosition = mapFragment?.cameraPosition - if (cameraPosition != null) { - val radius = sqrt(1000000 * ApplicationConstants.MIN_DOWNLOADABLE_AREA_IN_SQKM / PI) - return cameraPosition.position.enclosingBoundingBox(radius) - } - } - - return enclosingBBox - } - - /* -------------------------------------- Context Menu -------------------------------------- */ - - private fun onClickCreateNote(pos: LatLon) { - if ((mapFragment?.cameraPosition?.zoom ?: 0.0) < ApplicationConstants.NOTE_MIN_ZOOM) { - toast(R.string.create_new_note_unprecise) - return - } - - composeNote(pos) - } - - private fun composeNote(pos: LatLon, trackpoints: List? = null) { - mainBottomSheetViewModel.showCreateNote(trackpoints) - - mapFragment?.updateCameraPosition(300) { - position = pos - padding = getOpenQuestFormMapPadding() - } - } - - private fun onClickCreateTrack() { - mapFragment?.startPositionTrackRecording() - viewModel.isRecordingTracks.value = true - } - - //endregion - - //region Bottom Sheet - Controlling the bottom sheet and its interaction with the map - - /** Open or replace the bottom sheet. */ - private fun showBottomSheet(content: ShownBottomSheet) { - freezeMap() - } - - /** Make the map not follow the user's location anymore temporarily */ - private fun freezeMap() { - val mapFragment = mapFragment ?: return - if (wasFollowingPosition == null) wasFollowingPosition = mapFragment.isFollowingPosition - if (wasNavigationMode == null) wasNavigationMode = mapFragment.isNavigationMode - mapFragment.isFollowingPosition = false - mapFragment.isNavigationMode = false - } - - /** Make the map follow the user's location again (if it was following before) */ - private fun unfreezeMap() { - wasFollowingPosition?.let { mapFragment?.isFollowingPosition = it } - wasNavigationMode?.let { mapFragment?.isNavigationMode = it } - wasFollowingPosition = null - wasNavigationMode = null - } - - private fun clearHighlighting() { - mapFragment?.clearHighlighting() - } - - //endregion - - //region Bottom sheets - - @UiThread - private fun showOverlayForNewElementOnMap(overlay: Overlay) { - val mapFragment = mapFragment ?: return - - mapFragment.updateCameraPosition { - position = getCrosshairOffset()?.toPointF()?.let { mapFragment.getPositionAt(it) } - padding = getOpenQuestFormMapPadding() - } - mapFragment.hideNonHighlightedPins() - } - - @UiThread - private suspend fun showOverlayElementDetailsOnMap(overlay: Overlay, element: Element, geometry: ElementGeometry) { - val mapFragment = mapFragment ?: return - - mapFragment.updateCameraPosition { - padding = getOpenQuestFormMapPadding() - } - - mapFragment.highlightGeometry(geometry) - mapFragment.highlightPins(overlay.icon.toAndroidResourceId()!!, listOf(geometry.center)) - mapFragment.hideNonHighlightedPins() - } - - @UiThread - private fun showQuestDetailsOnMap(quest: Quest, element: Element?) { - val mapFragment = mapFragment ?: return - - if (quest is OsmQuest && element != null) { - showHighlightedElements(quest, element) - } - mapFragment.startFocus(quest.geometry, getOpenQuestFormMapPadding()) - mapFragment.highlightGeometry(quest.geometry) - mapFragment.highlightPins(quest.type.icon.toAndroidResourceId()!!, quest.markerLocations) - mapFragment.hideNonHighlightedPins(quest.key) - mapFragment.hideOverlay() - } - - private fun showHighlightedElements(quest: OsmQuest, element: Element) { - val bbox = quest.geometry.bounds.enlargedBy(quest.type.highlightedElementsRadius) - val lazyMapData = LazyMapDataWithGeometry(bbox, mapDataWithEditsSource) - - val levels = parseLevelsOrNull(element.tags) - - lifecycleScope.launch(Dispatchers.Default) { - val elements = withContext(Dispatchers.IO) { - quest.type.getHighlightedElements(element, lazyMapData) - } - val markers = elements.mapNotNull { e -> - // don't highlight "this" element - if (element == e) return@mapNotNull null - // include only elements with the same (=intersecting) level, if any - val eLevels = parseLevelsOrNull(e.tags) - if (!levels.levelsIntersect(eLevels)) return@mapNotNull null - // include only elements with the same layer, if any - if (element.tags["layer"] != e.tags["layer"]) return@mapNotNull null - - val geometry = lazyMapData.getGeometry(e.type, e.id) ?: return@mapNotNull null - val icon = getIcon(featureDictionary.value, e) - val title = getTitle(e.tags) - Marker(geometry, icon, title) - }.toList() - - withContext(Dispatchers.Main) { mapFragment?.setMarkersForCurrentHighlighting(markers) } - } - } - - private fun getCrosshairOffset(): Offset? { - val windowInfo = windowInfo ?: return null - val padding = getOpenQuestFormMapPadding() ?: return null - val size = windowInfo.containerSize - return Offset( - (padding.left + (size.width - padding.left - padding.right) / 2).toFloat(), - (padding.top + (size.height - padding.top - padding.bottom) / 2).toFloat() - ) - } - - private fun Offset.toPointF() = PointF(x, y) - - private fun getOpenQuestFormMapPadding(): Padding? { - val windowInfo = windowInfo ?: return null - val layoutDirection = if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) { - LayoutDirection.Rtl - } else { - LayoutDirection.Ltr - } - val density = Density(this) - return Dimensions.getOpenQuestFormMapPadding(windowInfo).toPadding(layoutDirection, density) - } - - //endregion - - companion object { - private const val TAG_MAP = "MainMapFragment" + if (intent.action == Intent.ACTION_VIEW) uri.value = intent.data?.toString() } } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4f97866baac..d8230831cf1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -197,6 +197,9 @@ kotlin { // UI widgets + // Map + implementation("org.maplibre.compose:maplibre-compose:0.16.0") + // non-lazy grid // NOTE: might replace with // https://developer.android.com/develop/ui/compose/layouts/adaptive/grid @@ -220,7 +223,6 @@ kotlin { } } androidMain { - kotlin.srcDirs(layout.buildDirectory.dir("generated/androidMain/kotlin")) dependencies { // Dependency injection implementation("io.insert-koin:koin-android") @@ -242,8 +244,8 @@ kotlin { // HTTP Client implementation("io.ktor:ktor-client-android:3.5.1") - // map and location - implementation("org.maplibre.gl:android-sdk-opengl:13.3.1") + // map + implementation("org.maplibre.compose:maplibre-compose-runtime-vulkan-android:0.16.0") // required to @Preview composables in Android Studio runtimeOnly("androidx.compose.ui:ui-tooling:1.10.0") @@ -395,13 +397,6 @@ tasks.register("updateChangelog") { targetFile = projectDir.resolve("src/commonMain/composeResources/files/changelog.html") } -tasks.register("updateMapStyle") { - group = "streetcomplete" - targetDir = projectDir.resolve("src/androidMain/assets/map_theme") - apiKey = "mL9X4SwxfsAGfojvGiion9hPKuGLKxPbogLyMbtakA2gJ3X88gcVlTSQ7OD6OfbZ" - mapStyleBranch = "master" -} - tasks.register("generateMetadataByCountry") { group = "streetcomplete" dependsOn( @@ -423,51 +418,6 @@ tasks.register("copyDefaultStringsToEnStrings") { } } -// necessary as long as map hasn't been converted to compose yet -tasks.register("copyIconsToAndroid") { - group = "streetcomplete" - sourceDir = projectDir.resolve("src/commonMain/composeResources/drawable") - targetDir = projectDir.resolve("build/generated/androidMain/res/drawable") - filter = { - // quest pins, icons for overlays - it.startsWith("quest_") || - it.startsWith("building_") || - it.startsWith("preset_") || - it == "sport_volleyball.xml" || - it == "religion_christian.xml" || - it == "religion_jewish.xml" || - it == "religion_muslim.xml" || - it == "address_dot.xml" || - it == "none.png" || - // icons for base map - it == "pin_shadow.png" || - it == "location_nyan.png" || - it == "scissors_cut.xml" || - it == "scissors.xml" || - it == "track_nyan.png" || - it == "track_nyan_record.png" || - it == "downloaded_area_hatching.xml" || - it == "location_shadow.xml" || - it == "location_view_direction.xml" || - it == "pin.xml" || - it == "pin_circle.xml" - } - indexFile = projectDir.resolve("build/generated/androidMain/kotlin/de/westnordost/streetcomplete/view/IconIndex.kt") -} - -tasks.register("copyStringsToAndroid") { - group = "streetcomplete" - sourceDir = projectDir.resolve("src/commonMain/composeResources") - targetDir = projectDir.resolve("build/generated/androidMain/res") -} - -project.afterEvaluate { - tasks.named("androidPreBuild") { - dependsOn(tasks.named("copyIconsToAndroid")) - dependsOn(tasks.named("copyStringsToAndroid")) - } -} - tasks.register("printQuestFiltersAsOverpassQL") { group = "utils" @@ -487,9 +437,3 @@ tasks.register("openingHoursParsingStatistics") { mainClass.set("de.westnordost.streetcomplete.OpeningHoursParsingStatisticsKt") } - -androidComponents { - onVariants { variant -> - variant.sources.res?.addStaticSourceDirectory(layout.buildDirectory.dir("generated/androidMain/res").get().asFile.absolutePath) - } -} diff --git a/app/src/androidMain/assets/authors.txt b/app/src/androidMain/assets/authors.txt deleted file mode 100644 index 58f7dac2458..00000000000 --- a/app/src/androidMain/assets/authors.txt +++ /dev/null @@ -1,4 +0,0 @@ -File License / Source (if not given, follow link for license info) -=============================================================================== - -map_theme/*.* GPL-3.0-only from https://github.com/streetcomplete/streetcomplete-mapstyle diff --git a/app/src/androidMain/assets/map_theme/sprites.json b/app/src/androidMain/assets/map_theme/sprites.json deleted file mode 100644 index c41021df1df..00000000000 --- a/app/src/androidMain/assets/map_theme/sprites.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "oneway-arrow": {"width": 32, "height": 56, "x": 0, "y": 0, "pixelRatio": 1, "sdf": true } -} \ No newline at end of file diff --git a/app/src/androidMain/assets/map_theme/sprites.png b/app/src/androidMain/assets/map_theme/sprites.png deleted file mode 100644 index ba76dd88948..00000000000 Binary files a/app/src/androidMain/assets/map_theme/sprites.png and /dev/null differ diff --git a/app/src/androidMain/assets/map_theme/sprites@2x.json b/app/src/androidMain/assets/map_theme/sprites@2x.json deleted file mode 100644 index efb810ae0c4..00000000000 --- a/app/src/androidMain/assets/map_theme/sprites@2x.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "oneway-arrow": {"width": 64, "height": 112, "x": 0, "y": 0, "pixelRatio": 2, "sdf": true } -} \ No newline at end of file diff --git a/app/src/androidMain/assets/map_theme/sprites@2x.png b/app/src/androidMain/assets/map_theme/sprites@2x.png deleted file mode 100644 index e0ead6ecea3..00000000000 Binary files a/app/src/androidMain/assets/map_theme/sprites@2x.png and /dev/null differ diff --git a/app/src/androidMain/assets/map_theme/streetcomplete-night.json b/app/src/androidMain/assets/map_theme/streetcomplete-night.json deleted file mode 100644 index 8d949169e66..00000000000 --- a/app/src/androidMain/assets/map_theme/streetcomplete-night.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "version": 8, - "name": "StreetComplete-Night", - "sources": { - "jawg-streets": { - "type": "vector", - "tiles": ["https://tile.jawg.io/streets-v2+hillshade-v1/{z}/{x}/{y}.pbf?access-token=mL9X4SwxfsAGfojvGiion9hPKuGLKxPbogLyMbtakA2gJ3X88gcVlTSQ7OD6OfbZ"], - "attribution": "© OSM contributors | © JawgMaps", - "maxzoom": 16 - } - }, - "transition": { "duration": 300, "delay": 0 }, - "light": { "intensity": 0.2 }, - "glyphs": "asset://map_theme/glyphs/{fontstack}/{range}.pbf", - "sprite": "asset://map_theme/sprites", - "layers": [ - { "id": "background", "type": "background", "paint": {"background-color": "#2e2e48"}}, - { "id": "landuse-town", "source": "jawg-streets", "source-layer": "landuse", "minzoom": 11.0, "filter": ["!", ["in", ["get", "class"], ["literal", ["pitch", "park", "grass", "cemetery", "wood", "scrub", "national_park"]]]], "type": "fill", "paint": { "fill-color": "#3d364e", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.0, 12.0, 1.0]} }, - { "id": "landuse-green", "source": "jawg-streets", "source-layer": "landuse", "minzoom": 5.0, "filter": ["in", ["get", "class"], ["literal", ["pitch", "park", "grass", "cemetery"]]], "type": "fill", "paint": { "fill-color": "#363054", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]} }, - { "id": "landuse-pitch-park-outline", "source": "jawg-streets", "source-layer": "landuse", "minzoom": 16.0, "filter": ["in", ["get", "class"], ["literal", ["pitch", "park"]]], "type": "line","paint": {"line-color": "#2e2e48", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-offset": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 0.5, 24.0, 64.0]} }, - { "id": "landuse-forest", "source": "jawg-streets", "source-layer": "landuse", "minzoom": 5.0, "filter": ["in", ["get", "class"], ["literal", ["wood", "scrub"]]], "type": "fill", "paint": { "fill-color": "#403962", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]} }, - { "id": "hillshade-highlight-1", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "highlight"], 1], "type": "fill", "paint": { "fill-color": "hsl(240, 30%, 50%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.12, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-highlight-2", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "highlight"], 2], "type": "fill", "paint": { "fill-color": "hsl(240, 30%, 50%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.12, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-shadow-1", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "shadow"], 1], "type": "fill", "paint": { "fill-color": "hsl(240, 80%, 0%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.05, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-shadow-2", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "shadow"], 2], "type": "fill", "paint": { "fill-color": "hsl(240, 80%, 0%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.05, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-shadow-3", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "shadow"], 3], "type": "fill", "paint": { "fill-color": "hsl(240, 80%, 0%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.05, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-shadow-4", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "shadow"], 4], "type": "fill", "paint": { "fill-color": "hsl(240, 80%, 0%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.05, 16.0, 0.0], "fill-antialias": false} }, - { "id": "water-areas", "source": "jawg-streets", "source-layer": "water", "filter": ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]], "type": "fill", "paint": { "fill-color": "#002"} }, - { "id": "water-shore-lines", "source": "jawg-streets", "source-layer": "water", "minzoom": 15.0, "filter": ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]], "type": "line","paint": {"line-color": "#228", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 1.0, 18.0, 4.0, 24.0, 256.0], "line-offset": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 1.0, 18.0, 4.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 18.0, 1.0]},"layout": {"line-miter-limit": 6} }, - { "id": "rivers", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 10.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["river", "canal"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#002", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 10.0, 1.0, 16.0, 3.0, 24.0, 768.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "streams", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 10.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["stream", "ditch", "drain"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#002", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 256.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "aeroways", "source": "jawg-streets", "source-layer": "aeroway", "filter": ["==", ["geometry-type"], "LineString"], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 10.0, 1.0, 24.0, 8192.0]},"layout": {"line-join": "round"} }, - { "id": "buildings", "source": "jawg-streets", "source-layer": "building", "minzoom": 15.0, "type": "fill", "paint": { "fill-color": "rgba(41,92,92,0.8)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]} }, - { "id": "buildings-outline", "source": "jawg-streets", "source-layer": "building", "minzoom": 15.5, "type": "line","paint": {"line-color": "rgba(31,82,82,0.8)", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.5, 0.0, 16.0, 1.0]} }, - { "id": "pedestrian-areas-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 16.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["path", "street_limited"]]], ["==", ["geometry-type"], "Polygon"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-offset": ["interpolate", ["exponential", 2], ["zoom"], 16.0, -0.5, 24.0, -64.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 0.0, 17.0, 1.0]} }, - { "id": "pedestrian-areas", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["path", "street_limited"]]], ["==", ["geometry-type"], "Polygon"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "fill", "paint": { "fill-color": "#554e7e", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]} }, - { "id": "pedestrian-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-service-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-minor-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-major-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "motorways-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#99f", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "motorway-links-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#99f", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "paths-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.5, 16.0, 1.0, 24.0, 256.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "steps-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["path"]]], ["in", ["get", "type"], ["literal", ["steps"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#554e7e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.35, 16.0, 0.7, 24.0, 179.2], "line-dasharray": [0.6, 0.4], "line-opacity": 0.25} }, - { "id": "pedestrian-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#554e7e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-minor-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-major-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorways-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#669", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorway-links-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#669", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "paths-tunnel-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#3d364e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.25, 16.0, 0.5, 24.0, 128.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-tunnel-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#3d364e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.25, 16.0, 1.5, 24.0, 384.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "pedestrian-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-minor-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-major-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorways-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#99f", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorway-links-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#99f", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "paths", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.5, 16.0, 1.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "steps", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["path"]]], ["in", ["get", "type"], ["literal", ["steps"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#554e7e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.35, 16.0, 0.7, 24.0, 179.2], "line-dasharray": [0.6, 0.4]} }, - { "id": "pedestrian", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#554e7e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.0, 15.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.0, 15.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-minor", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 13.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-major", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorways", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#669", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorway-links", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#669", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "paths-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#3d364e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.25, 16.0, 0.5, 24.0, 128.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#3d364e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.25, 16.0, 1.5, 24.0, 384.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "railways", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["major_rail", "minor_rail"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#96c", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.75, 13.0, 2.0, 16.999, 4.0, 17.0, 2.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 17.0, 0.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 13.0, 1.0]},"layout": {"line-join": "round"} }, - { "id": "barriers-large", "source": "jawg-streets", "source-layer": "structure", "minzoom": 16.0, "filter": ["in", ["get", "type"], ["literal", ["city_wall", "dam", "cliff"]]], "type": "line","paint": {"line-color": "rgba(31,82,82,0.8)", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 4.0, 24.0, 768.0]} }, - { "id": "barriers-wall", "source": "jawg-streets", "source-layer": "structure", "minzoom": 16.0, "filter": ["all", ["==", ["get", "class"], "fence"], ["!=", ["get", "type"], "city_wall"]], "type": "line","paint": {"line-color": "rgba(31,82,82,0.8)", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 256.0]} }, - { "id": "barriers-hedges", "source": "jawg-streets", "source-layer": "structure", "minzoom": 16.0, "filter": ["==", ["get", "class"], "hedge"], "type": "line","paint": {"line-color": "#403962", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 512.0]} }, - { "id": "point-barriers", "source": "jawg-streets", "source-layer": "structure", "minzoom": 17.0, "filter": ["==", ["geometry-type"], "Point"], "type": "circle", "paint": {"circle-color": "#99f", "circle-radius": ["interpolate", ["exponential", 2], ["zoom"], 17.0, 2.0, 24.0, 256.0]} }, - { "id": "bridge-areas", "source": "jawg-streets", "source-layer": "structure", "filter": ["all", ["==", ["geometry-type"], "Polygon"], ["==", ["get", "class"], "bridge"]], "type": "fill", "paint": { "fill-color": "rgba(41,92,92,0.8)", "fill-opacity": 0.8} }, - { "id": "bridge-lines", "source": "jawg-streets", "source-layer": "structure", "filter": ["all", ["==", ["geometry-type"], "LineString"], ["==", ["get", "class"], "bridge"]], "type": "line","paint": {"line-color": "rgba(41,92,92,0.8)", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 4.0, 24.0, 512.0], "line-opacity": 0.8} }, - { "id": "water-areas-bridge", "source": "jawg-streets", "source-layer": "water", "filter": ["==", ["get", "structure"], "bridge"], "type": "fill", "paint": { "fill-color": "#002"} }, - { "id": "rivers-bridge", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 10.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["river", "canal"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#002", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 10.0, 1.0, 16.0, 3.0, 24.0, 768.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "streams-bridge", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 10.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["stream", "ditch", "drain"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#002", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 256.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "pedestrian-areas-casing-bridge", "source": "jawg-streets", "source-layer": "road", "minzoom": 16.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["path", "street_limited"]]], ["==", ["geometry-type"], "Polygon"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-offset": ["interpolate", ["exponential", 2], ["zoom"], 16.0, -0.5, 24.0, -64.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 0.0, 17.0, 1.0]} }, - { "id": "pedestrian-areas-bridge", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["path", "street_limited"]]], ["==", ["geometry-type"], "Polygon"], ["==", ["get", "structure"], "bridge"]], "type": "fill", "paint": { "fill-color": "#554e7e", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]} }, - { "id": "pedestrian-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-service-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-minor-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-major-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "motorways-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#99f", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "motorway-links-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#99f", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "paths-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#547", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.5, 16.0, 1.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "steps-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["path"]]], ["in", ["get", "type"], ["literal", ["steps"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#554e7e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.35, 16.0, 0.7, 24.0, 179.2], "line-dasharray": [0.6, 0.4]} }, - { "id": "pedestrian-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#554e7e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.0, 15.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.0, 15.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-minor-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 13.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-major-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#559", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorways-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#669", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorway-links-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#669", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "paths-bridge-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#3d364e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.25, 16.0, 0.5, 24.0, 128.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-bridge-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#3d364e", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.25, 16.0, 1.5, 24.0, 384.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "railways-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["major_rail", "minor_rail"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#96c", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.75, 13.0, 2.0, 16.999, 4.0, 17.0, 2.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 17.0, 0.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 13.0, 1.0]},"layout": {"line-join": "round"} }, - { "id": "oneway-arrows", "source": "jawg-streets", "source-layer": "road", "minzoom": 17.0, "filter": ["all", ["==", ["geometry-type"], "LineString"], ["==", ["get", "oneway"], true]], "type": "symbol", "paint": {"icon-color": "#ccf"}, "layout": {"icon-image": "oneway-arrow", "icon-size": ["interpolate", ["exponential", 2], ["zoom"], 17.0, 0.25, 24.0, 16.0], "symbol-spacing": ["interpolate", ["exponential", 2], ["zoom"], 17.0, 200.0, 24.0, 25600.0], "symbol-placement": "line", "icon-padding": 5, "icon-rotate": 90, "icon-rotation-alignment": "map"} }, - { "id": "boundaries", "source": "jawg-streets", "source-layer": "admin", "filter": ["all", ["==", ["get", "admin_level"], 2], ["!=", ["get", "maritime"], true]], "type": "line","paint": {"line-color": "#e72", "line-width": 1, "line-dasharray": [1, 2]} }, - { "id": "labels-country", "source": "jawg-streets", "source-layer": "place_label", "filter": ["==", ["get", "class"], "country"], "type": "symbol", "paint": {"text-color": "#ccf", "text-halo-color": "#2e2e48", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Bold"], "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-localities", "source": "jawg-streets", "source-layer": "place_label", "filter": ["==", ["get", "class"], "locality"], "type": "symbol", "paint": {"text-color": "#ccf", "text-halo-color": "#2e2e48", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-housenumbers", "source": "jawg-streets", "source-layer": "housenum_label", "minzoom": 17.0, "type": "symbol", "paint": {"text-color": "#ccf", "text-halo-color": "#2e2e48", "text-halo-width": 2.5}, "layout": {"text-field": ["get", "house_num"], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-road", "source": "jawg-streets", "source-layer": "road", "minzoom": 14.0, "filter": ["==", ["geometry-type"], "LineString"], "type": "symbol", "paint": {"text-color": "#ccf", "text-halo-color": "#2e2e48", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "symbol-placement": "line-center", "text-padding": 12, "text-max-width": 25, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-rivers", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 14.0, "filter": ["all", ["!=", ["get", "structure"], "tunnel"], ["in", ["get", "class"], ["literal", ["river", "canal"]]]], "type": "symbol", "paint": {"text-color": "#2e2e48", "text-halo-color": "#ccf", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "symbol-placement": "line-center", "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-streams", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 16.0, "filter": ["all", ["!=", ["get", "structure"], "tunnel"], ["in", ["get", "class"], ["literal", ["stream", "ditch", "drain"]]]], "type": "symbol", "paint": {"text-color": "#2e2e48", "text-halo-color": "#ccf", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "symbol-placement": "line-center", "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} } - ] -} diff --git a/app/src/androidMain/assets/map_theme/streetcomplete.json b/app/src/androidMain/assets/map_theme/streetcomplete.json deleted file mode 100644 index 2943023876e..00000000000 --- a/app/src/androidMain/assets/map_theme/streetcomplete.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "version": 8, - "name": "StreetComplete", - "sources": { - "jawg-streets": { - "type": "vector", - "tiles": ["https://tile.jawg.io/streets-v2+hillshade-v1/{z}/{x}/{y}.pbf?access-token=mL9X4SwxfsAGfojvGiion9hPKuGLKxPbogLyMbtakA2gJ3X88gcVlTSQ7OD6OfbZ"], - "attribution": "© OSM contributors | © JawgMaps", - "maxzoom": 16 - } - }, - "transition": { "duration": 300, "delay": 0 }, - "light": { "intensity": 0.2 }, - "glyphs": "asset://map_theme/glyphs/{fontstack}/{range}.pbf", - "sprite": "asset://map_theme/sprites", - "layers": [ - { "id": "background", "type": "background", "paint": {"background-color": "#f3eacc"}}, - { "id": "landuse-town", "source": "jawg-streets", "source-layer": "landuse", "minzoom": 11.0, "filter": ["!", ["in", ["get", "class"], ["literal", ["pitch", "park", "grass", "cemetery", "wood", "scrub", "national_park"]]]], "type": "fill", "paint": { "fill-color": "#f3dacd", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.0, 12.0, 1.0]} }, - { "id": "landuse-green", "source": "jawg-streets", "source-layer": "landuse", "minzoom": 5.0, "filter": ["in", ["get", "class"], ["literal", ["pitch", "park", "grass", "cemetery"]]], "type": "fill", "paint": { "fill-color": "#c6ddaa", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]} }, - { "id": "landuse-pitch-park-outline", "source": "jawg-streets", "source-layer": "landuse", "minzoom": 16.0, "filter": ["in", ["get", "class"], ["literal", ["pitch", "park"]]], "type": "line","paint": {"line-color": "#f3eacc", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-offset": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 0.5, 24.0, 64.0]} }, - { "id": "landuse-forest", "source": "jawg-streets", "source-layer": "landuse", "minzoom": 5.0, "filter": ["in", ["get", "class"], ["literal", ["wood", "scrub"]]], "type": "fill", "paint": { "fill-color": "#a8c884", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]} }, - { "id": "hillshade-highlight-1", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "highlight"], 1], "type": "fill", "paint": { "fill-color": "hsl(220, 100%, 95%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.12, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-highlight-2", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "highlight"], 2], "type": "fill", "paint": { "fill-color": "hsl(220, 100%, 95%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.12, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-shadow-1", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "shadow"], 1], "type": "fill", "paint": { "fill-color": "hsl(18, 100%, 40%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.05, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-shadow-2", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "shadow"], 2], "type": "fill", "paint": { "fill-color": "hsl(18, 100%, 40%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.05, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-shadow-3", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "shadow"], 3], "type": "fill", "paint": { "fill-color": "hsl(18, 100%, 40%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.05, 16.0, 0.0], "fill-antialias": false} }, - { "id": "hillshade-shadow-4", "source": "jawg-streets", "source-layer": "hillshade", "maxzoom": 16.0, "filter": ["==", ["get", "shadow"], 4], "type": "fill", "paint": { "fill-color": "hsl(18, 100%, 40%)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.05, 16.0, 0.0], "fill-antialias": false} }, - { "id": "water-areas", "source": "jawg-streets", "source-layer": "water", "filter": ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]], "type": "fill", "paint": { "fill-color": "#68d"} }, - { "id": "water-shore-lines", "source": "jawg-streets", "source-layer": "water", "minzoom": 15.0, "filter": ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]], "type": "line","paint": {"line-color": "#abe", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 1.0, 18.0, 4.0, 24.0, 256.0], "line-offset": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 1.0, 18.0, 4.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 18.0, 1.0]},"layout": {"line-miter-limit": 6} }, - { "id": "rivers", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 10.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["river", "canal"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#68d", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 10.0, 1.0, 16.0, 3.0, 24.0, 768.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "streams", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 10.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["stream", "ditch", "drain"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#68d", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 256.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "aeroways", "source": "jawg-streets", "source-layer": "aeroway", "filter": ["==", ["geometry-type"], "LineString"], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 10.0, 1.0, 24.0, 8192.0]},"layout": {"line-join": "round"} }, - { "id": "buildings", "source": "jawg-streets", "source-layer": "building", "minzoom": 15.0, "type": "fill", "paint": { "fill-color": "rgb(204,214,238)", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]} }, - { "id": "buildings-outline", "source": "jawg-streets", "source-layer": "building", "minzoom": 15.5, "type": "line","paint": {"line-color": "rgb(185,195,217)", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.5, 0.0, 16.0, 1.0]} }, - { "id": "pedestrian-areas-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 16.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["path", "street_limited"]]], ["==", ["geometry-type"], "Polygon"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-offset": ["interpolate", ["exponential", 2], ["zoom"], 16.0, -0.5, 24.0, -64.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 0.0, 17.0, 1.0]} }, - { "id": "pedestrian-areas", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["path", "street_limited"]]], ["==", ["geometry-type"], "Polygon"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "fill", "paint": { "fill-color": "#f6eee6", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]} }, - { "id": "pedestrian-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-service-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-minor-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-major-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "motorways-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#a88", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "motorway-links-tunnel-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#a88", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0], "line-dasharray": [4, 4], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "paths-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.5, 16.0, 1.0, 24.0, 256.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "steps-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["path"]]], ["in", ["get", "type"], ["literal", ["steps"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#f6eee6", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.35, 16.0, 0.7, 24.0, 179.2], "line-dasharray": [0.6, 0.4], "line-opacity": 0.25} }, - { "id": "pedestrian-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#f6eee6", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-minor-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-major-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorways-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#fa8", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorway-links-tunnel", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#fa8", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0], "line-opacity": 0.25},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "paths-tunnel-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#f3dacd", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.25, 16.0, 0.5, 24.0, 128.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-tunnel-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["==", ["get", "structure"], "tunnel"]], "type": "line","paint": {"line-color": "#f3dacd", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.25, 16.0, 1.5, 24.0, 384.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "pedestrian-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-minor-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-major-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorways-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#a88", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorway-links-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#a88", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "paths", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.5, 16.0, 1.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "steps", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["path"]]], ["in", ["get", "type"], ["literal", ["steps"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#f6eee6", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.35, 16.0, 0.7, 24.0, 179.2], "line-dasharray": [0.6, 0.4]} }, - { "id": "pedestrian", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#f6eee6", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.0, 15.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.0, 15.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-minor", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 13.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-major", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorways", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#fa8", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorway-links", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#fa8", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "paths-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#f3dacd", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.25, 16.0, 0.5, 24.0, 128.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#f3dacd", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.25, 16.0, 1.5, 24.0, 384.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "railways", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["major_rail", "minor_rail"]]], ["==", ["geometry-type"], "LineString"], ["!", ["in", ["get", "structure"], ["literal", ["bridge", "tunnel"]]]]], "type": "line","paint": {"line-color": "#99a", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.75, 13.0, 2.0, 16.999, 4.0, 17.0, 2.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 17.0, 0.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 13.0, 1.0]},"layout": {"line-join": "round"} }, - { "id": "barriers-large", "source": "jawg-streets", "source-layer": "structure", "minzoom": 16.0, "filter": ["in", ["get", "type"], ["literal", ["city_wall", "dam", "cliff"]]], "type": "line","paint": {"line-color": "rgb(185,195,217)", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 4.0, 24.0, 768.0]} }, - { "id": "barriers-wall", "source": "jawg-streets", "source-layer": "structure", "minzoom": 16.0, "filter": ["all", ["==", ["get", "class"], "fence"], ["!=", ["get", "type"], "city_wall"]], "type": "line","paint": {"line-color": "rgb(185,195,217)", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 256.0]} }, - { "id": "barriers-hedges", "source": "jawg-streets", "source-layer": "structure", "minzoom": 16.0, "filter": ["==", ["get", "class"], "hedge"], "type": "line","paint": {"line-color": "#a8c884", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 512.0]} }, - { "id": "point-barriers", "source": "jawg-streets", "source-layer": "structure", "minzoom": 17.0, "filter": ["==", ["geometry-type"], "Point"], "type": "circle", "paint": {"circle-color": "#888", "circle-radius": ["interpolate", ["exponential", 2], ["zoom"], 17.0, 2.0, 24.0, 256.0]} }, - { "id": "bridge-areas", "source": "jawg-streets", "source-layer": "structure", "filter": ["all", ["==", ["geometry-type"], "Polygon"], ["==", ["get", "class"], "bridge"]], "type": "fill", "paint": { "fill-color": "rgb(204,214,238)", "fill-opacity": 0.8} }, - { "id": "bridge-lines", "source": "jawg-streets", "source-layer": "structure", "filter": ["all", ["==", ["geometry-type"], "LineString"], ["==", ["get", "class"], "bridge"]], "type": "line","paint": {"line-color": "rgb(204,214,238)", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 4.0, 24.0, 512.0], "line-opacity": 0.8} }, - { "id": "water-areas-bridge", "source": "jawg-streets", "source-layer": "water", "filter": ["==", ["get", "structure"], "bridge"], "type": "fill", "paint": { "fill-color": "#68d"} }, - { "id": "rivers-bridge", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 10.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["river", "canal"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#68d", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 10.0, 1.0, 16.0, 3.0, 24.0, 768.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "streams-bridge", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 10.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["stream", "ditch", "drain"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#68d", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 256.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "pedestrian-areas-casing-bridge", "source": "jawg-streets", "source-layer": "road", "minzoom": 16.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["path", "street_limited"]]], ["==", ["geometry-type"], "Polygon"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-offset": ["interpolate", ["exponential", 2], ["zoom"], 16.0, -0.5, 24.0, -64.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 0.0, 17.0, 1.0]} }, - { "id": "pedestrian-areas-bridge", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.0, "filter": ["all", ["in", ["get", "class"], ["literal", ["path", "street_limited"]]], ["==", ["geometry-type"], "Polygon"], ["==", ["get", "structure"], "bridge"]], "type": "fill", "paint": { "fill-color": "#f6eee6", "fill-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]} }, - { "id": "pedestrian-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-service-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-minor-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "roads-major-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "motorways-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#a88", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "motorway-links-bridge-casing", "source": "jawg-streets", "source-layer": "road", "minzoom": 15.5, "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#a88", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 16.0, 1.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "butt", "line-join": "round"} }, - { "id": "paths-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#ca9", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.5, 16.0, 1.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 15.0, 0.0, 16.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "steps-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["path"]]], ["in", ["get", "type"], ["literal", ["steps"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#f6eee6", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.35, 16.0, 0.7, 24.0, 179.2], "line-dasharray": [0.6, 0.4]} }, - { "id": "pedestrian-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "street_limited"], ["==", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#f6eee6", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 1.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.0, 15.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.5, 16.0, 3.0, 24.0, 768.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.0, 15.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-minor-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["street", "street_limited"]]], ["!=", ["get", "type"], "pedestrian"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 0.5, 16.0, 4.0, 24.0, 1024.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 13.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-major-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "main"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#fff", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 9.0, 1.0, 16.0, 6.0, 24.0, 1536.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorways-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#fa8", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 8.0, 1.0, 16.0, 8.0, 24.0, 2048.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 5.0, 0.0, 6.0, 1.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "motorway-links-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "motorway_link"], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#fa8", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 11.0, 1.0, 16.0, 4.0, 24.0, 1024.0]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "paths-bridge-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["==", ["get", "class"], "path"], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#f3dacd", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 14.0, 0.25, 16.0, 0.5, 24.0, 128.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "roads-service-bridge-private", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["service", "driveway"]]], ["==", ["geometry-type"], "LineString"], ["in", ["get", "access"], ["literal", ["no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency"]]], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#f3dacd", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 13.0, 0.25, 16.0, 1.5, 24.0, 384.0], "line-dasharray": [1, 2]},"layout": {"line-cap": "round", "line-join": "round"} }, - { "id": "railways-bridge", "source": "jawg-streets", "source-layer": "road", "filter": ["all", ["in", ["get", "class"], ["literal", ["major_rail", "minor_rail"]]], ["==", ["geometry-type"], "LineString"], ["==", ["get", "structure"], "bridge"]], "type": "line","paint": {"line-color": "#99a", "line-width": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.75, 13.0, 2.0, 16.999, 4.0, 17.0, 2.0, 24.0, 128.0], "line-gap-width": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 17.0, 0.0, 24.0, 256.0], "line-opacity": ["interpolate", ["exponential", 2], ["zoom"], 12.0, 0.0, 13.0, 1.0]},"layout": {"line-join": "round"} }, - { "id": "oneway-arrows", "source": "jawg-streets", "source-layer": "road", "minzoom": 17.0, "filter": ["all", ["==", ["geometry-type"], "LineString"], ["==", ["get", "oneway"], true]], "type": "symbol", "paint": {"icon-color": "#888"}, "layout": {"icon-image": "oneway-arrow", "icon-size": ["interpolate", ["exponential", 2], ["zoom"], 17.0, 0.25, 24.0, 16.0], "symbol-spacing": ["interpolate", ["exponential", 2], ["zoom"], 17.0, 200.0, 24.0, 25600.0], "symbol-placement": "line", "icon-padding": 5, "icon-rotate": 90, "icon-rotation-alignment": "map"} }, - { "id": "boundaries", "source": "jawg-streets", "source-layer": "admin", "filter": ["all", ["==", ["get", "admin_level"], 2], ["!=", ["get", "maritime"], true]], "type": "line","paint": {"line-color": "#e39", "line-width": 1, "line-dasharray": [1, 2]} }, - { "id": "labels-country", "source": "jawg-streets", "source-layer": "place_label", "filter": ["==", ["get", "class"], "country"], "type": "symbol", "paint": {"text-color": "#124", "text-halo-color": "#fff", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Bold"], "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-localities", "source": "jawg-streets", "source-layer": "place_label", "filter": ["==", ["get", "class"], "locality"], "type": "symbol", "paint": {"text-color": "#124", "text-halo-color": "#fff", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-housenumbers", "source": "jawg-streets", "source-layer": "housenum_label", "minzoom": 17.0, "type": "symbol", "paint": {"text-color": "#124", "text-halo-color": "#fff", "text-halo-width": 2.5}, "layout": {"text-field": ["get", "house_num"], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-road", "source": "jawg-streets", "source-layer": "road", "minzoom": 14.0, "filter": ["==", ["geometry-type"], "LineString"], "type": "symbol", "paint": {"text-color": "#124", "text-halo-color": "#fff", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "symbol-placement": "line-center", "text-padding": 12, "text-max-width": 25, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-rivers", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 14.0, "filter": ["all", ["!=", ["get", "structure"], "tunnel"], ["in", ["get", "class"], ["literal", ["river", "canal"]]]], "type": "symbol", "paint": {"text-color": "#fff", "text-halo-color": "#349", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "symbol-placement": "line-center", "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} }, - { "id": "labels-streams", "source": "jawg-streets", "source-layer": "waterway", "minzoom": 16.0, "filter": ["all", ["!=", ["get", "structure"], "tunnel"], ["in", ["get", "class"], ["literal", ["stream", "ditch", "drain"]]]], "type": "symbol", "paint": {"text-color": "#fff", "text-halo-color": "#349", "text-halo-width": 2.5}, "layout": {"text-field": ["coalesce", ["get", "name"]], "text-size": ["interpolate", ["exponential", 2], ["zoom"], 1.0, 13.0, 24.0, 64.0], "text-font": ["Roboto Regular"], "symbol-placement": "line-center", "text-padding": 12, "symbol-sort-key": ["get", "scalerank"]} } - ] -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/AndroidModule.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/AndroidModule.kt index 5e63d514dc9..b180796f485 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/AndroidModule.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/AndroidModule.kt @@ -18,8 +18,8 @@ import de.westnordost.streetcomplete.data.download.AndroidDownloadController import de.westnordost.streetcomplete.data.download.DownloadController import de.westnordost.streetcomplete.data.download.DownloadWorker import de.westnordost.streetcomplete.data.initialize +import de.westnordost.streetcomplete.data.maptiles.MapLibreMapTilesDownloader import de.westnordost.streetcomplete.data.maptiles.MapTilesDownloader -import de.westnordost.streetcomplete.data.maptiles.MapTilesDownloaderAndroid import de.westnordost.streetcomplete.data.osm.edits.upload.changesets.AndroidChangesetAutoCloser import de.westnordost.streetcomplete.data.osm.edits.upload.changesets.ChangesetAutoCloser import de.westnordost.streetcomplete.data.osm.edits.upload.changesets.ChangesetAutoCloserWorker @@ -28,10 +28,6 @@ import de.westnordost.streetcomplete.data.upload.UploadController import de.westnordost.streetcomplete.data.upload.UploadWorker import de.westnordost.streetcomplete.screens.about.AndroidAppStoreInfo import de.westnordost.streetcomplete.screens.about.AppStoreInfo -import de.westnordost.streetcomplete.screens.main.AndroidEmailAppLauncher -import de.westnordost.streetcomplete.screens.main.AndroidMapAppLauncher -import de.westnordost.streetcomplete.screens.main.EmailAppLauncher -import de.westnordost.streetcomplete.screens.main.MapAppLauncher import de.westnordost.streetcomplete.ui.util.measure.AndroidArSupportChecker import de.westnordost.streetcomplete.ui.util.measure.ArSupportChecker import de.westnordost.streetcomplete.util.error_reporting.CrashReportHolder @@ -46,10 +42,15 @@ import org.koin.androidx.workmanager.dsl.worker import org.koin.core.qualifier.named import org.koin.dsl.module import org.koin.dsl.onClose +import org.maplibre.compose.location.AndroidHeadingProvider import org.maplibre.compose.location.AndroidLocationProvider import org.maplibre.compose.location.AndroidSystemSettingsLauncher +import org.maplibre.compose.location.HeadingProvider import org.maplibre.compose.location.LocationProvider import org.maplibre.compose.location.SystemSettingsLauncher +import org.maplibre.compose.map.MapRuntime +import org.maplibre.compose.map.MapRuntimeOptions +import org.maplibre.compose.map.createMapRuntime private const val COMPOSE_FILES_DIR = "composeResources/de.westnordost.streetcomplete.resources/files" @@ -100,6 +101,7 @@ val androidModule = module { // location factory { AndroidLocationProvider(get()) } + factory { AndroidHeadingProvider(get()) } factory { AndroidSystemSettingsLauncher(get()) } // settings @@ -114,6 +116,21 @@ val androidModule = module { factory { AndroidActiveNetworkConnection(androidContext()) } + // map + + single { + // Keep the existing offline packs and ambient tiles when migrating from the Android SDK. + val cacheFile = Path(androidContext().filesDir.path, "mbgl-offline.db") + createMapRuntime(MapRuntimeOptions(cacheFile = cacheFile)) + } onClose { it?.close() } + + factory { + MapLibreMapTilesDownloader( + get().offlineManager, + androidContext().resources.displayMetrics.density + ) + } + // background jobs single { AndroidUploadController(androidContext()) } @@ -127,6 +144,4 @@ val androidModule = module { factory { AndroidPeriodicCleaner(androidContext()) } worker { CleanerWorker(get(), get(), get()) } - - factory { MapTilesDownloaderAndroid(androidContext()) } } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloaderAndroid.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloaderAndroid.kt deleted file mode 100644 index f6af2829123..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloaderAndroid.kt +++ /dev/null @@ -1,64 +0,0 @@ -package de.westnordost.streetcomplete.data.maptiles - -import android.content.Context -import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox -import de.westnordost.streetcomplete.screens.main.map.maplibre.awaitCreateOfflineRegion -import de.westnordost.streetcomplete.screens.main.map.maplibre.awaitDownload -import de.westnordost.streetcomplete.screens.main.map.maplibre.awaitResetDatabase -import de.westnordost.streetcomplete.screens.main.map.maplibre.toLatLng -import de.westnordost.streetcomplete.util.ktx.format -import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds -import de.westnordost.streetcomplete.util.logs.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.maplibre.android.MapLibre -import org.maplibre.android.geometry.LatLngBounds -import org.maplibre.android.offline.OfflineManager -import org.maplibre.android.offline.OfflineTilePyramidRegionDefinition - -class MapTilesDownloaderAndroid(private val context: Context) : MapTilesDownloader { - - init { - try { - // must be called before getting OfflineManager instance but will throw an exception if - // not called from the main thread - MapLibre.getInstance(context) - } catch (_: Exception) { } - } - - override suspend fun clear() { - try { - OfflineManager.getInstance(context).awaitResetDatabase() - } catch (e: Exception) { - Log.w(TAG, e.message.orEmpty(), e) - } - } - - override suspend fun download(bbox: BoundingBox) = withContext(Dispatchers.IO) { - val bounds = LatLngBounds.fromLatLngs(listOf(bbox.max.toLatLng(), bbox.min.toLatLng())) - val pixelRatio = context.resources.displayMetrics.density - val regionDefinition = OfflineTilePyramidRegionDefinition(styleUrl, bounds, 0.0, 16.0, pixelRatio) - - // store timestamp as metadata for deleting areas older than - // (could also be done directly from the long) - val metadata = nowAsEpochMilliseconds().toString().toByteArray(Charsets.UTF_8) - try { - val offlineRegion = OfflineManager.getInstance(context).awaitCreateOfflineRegion(regionDefinition, metadata) - val time = nowAsEpochMilliseconds() - val status = offlineRegion.awaitDownload() - val seconds = (nowAsEpochMilliseconds() - time) / 1000.0 - Log.i(TAG, "Downloaded ${status.completedTileCount} tiles (${status.completedTileSize / 1000}kB) in ${seconds.format(1)}s") - // note that the numbers include tiles that were already on device - // no idea how to check which tiles were really downloaded (other than in android log for MapLibre) - // status.requiredResourceCount and status.completedResourceSize might be interesting too - } catch (e: Exception) { - Log.w(TAG, e.message.orEmpty(), e) - } - } - - companion object { - private const val TAG = "MapTilesDownload" - - private const val styleUrl = "https://streetcomplete.app/map-jawg/streetcomplete.json" - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.android.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.android.kt deleted file mode 100644 index 9614f9fda35..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.android.kt +++ /dev/null @@ -1,23 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.controls - -import android.icu.util.LocaleData -import android.icu.util.ULocale -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalConfiguration -import de.westnordost.streetcomplete.screens.main.controls.ScaleBarMeasure.FeetAndMiles -import de.westnordost.streetcomplete.screens.main.controls.ScaleBarMeasure.Metric -import de.westnordost.streetcomplete.screens.main.controls.ScaleBarMeasure.YardsAndMiles - -/** use system locale APIs for the primary scale bar measure */ -@Composable -internal actual fun systemDefaultPrimaryMeasure(): ScaleBarMeasure? { - if (android.os.Build.VERSION.SDK_INT < 28) return null - val locales = LocalConfiguration.current.locales - if (locales.isEmpty) return null - return when (LocaleData.getMeasurementSystem(ULocale.forLocale(locales[0]))) { - LocaleData.MeasurementSystem.SI -> Metric - LocaleData.MeasurementSystem.US -> FeetAndMiles - LocaleData.MeasurementSystem.UK -> YardsAndMiles - else -> null - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/DownloadedAreaManager.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/DownloadedAreaManager.kt deleted file mode 100644 index 677af787efd..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/DownloadedAreaManager.kt +++ /dev/null @@ -1,51 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map - -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import de.westnordost.streetcomplete.ApplicationConstants -import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesSource -import de.westnordost.streetcomplete.screens.main.map.components.DownloadedAreaMapComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DownloadedAreaManager( - private val mapComponent: DownloadedAreaMapComponent, - private val downloadedTilesSource: DownloadedTilesSource -) : DefaultLifecycleObserver { - - private val viewLifecycleScope: CoroutineScope = CoroutineScope(SupervisorJob()) - - private val downloadedTilesListener = object : DownloadedTilesSource.Listener { - override fun onUpdated() { - update() - } - } - - override fun onStart(owner: LifecycleOwner) { - super.onStart(owner) - update() - downloadedTilesSource.addListener(downloadedTilesListener) - } - - override fun onStop(owner: LifecycleOwner) { - super.onStop(owner) - viewLifecycleScope.coroutineContext.cancelChildren() - downloadedTilesSource.removeListener(downloadedTilesListener) - } - - override fun onDestroy(owner: LifecycleOwner) { - viewLifecycleScope.cancel() - } - - private fun update() { - viewLifecycleScope.launch { - val tiles = withContext(Dispatchers.IO) { downloadedTilesSource.getAll(ApplicationConstants.DELETE_OLD_DATA_AFTER) } - mapComponent.set(tiles) - } - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/EditHistoryPinsManager.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/EditHistoryPinsManager.kt deleted file mode 100644 index 2bdd6ad8be0..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/EditHistoryPinsManager.kt +++ /dev/null @@ -1,154 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map - -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import de.westnordost.streetcomplete.data.edithistory.Edit -import de.westnordost.streetcomplete.data.edithistory.EditHistorySource -import de.westnordost.streetcomplete.data.edithistory.EditKey -import de.westnordost.streetcomplete.data.edithistory.ElementEditKey -import de.westnordost.streetcomplete.data.edithistory.NoteEditKey -import de.westnordost.streetcomplete.data.edithistory.QuestHiddenKey -import de.westnordost.streetcomplete.data.osm.edits.ElementEdit -import de.westnordost.streetcomplete.data.osm.mapdata.ElementType -import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuestHidden -import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEdit -import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestHidden -import de.westnordost.streetcomplete.data.quest.OsmNoteQuestKey -import de.westnordost.streetcomplete.data.quest.OsmQuestKey -import de.westnordost.streetcomplete.screens.main.edithistory.icon -import de.westnordost.streetcomplete.screens.main.map.components.Pin -import de.westnordost.streetcomplete.screens.main.map.components.PinsMapComponent -import de.westnordost.streetcomplete.view.toAndroidResourceId -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class EditHistoryPinsManager( - private val pinsMapComponent: PinsMapComponent, - private val editHistorySource: EditHistorySource, -) : DefaultLifecycleObserver { - - private val viewLifecycleScope: CoroutineScope = CoroutineScope(SupervisorJob()) - - /** Switch visibility of edit history pins layer */ - var isVisible: Boolean = false - set(value) { - if (field == value) return - field = value - if (value) show() else hide() - } - - private var isStarted: Boolean = false - - private val editHistoryListener = object : EditHistorySource.Listener { - override fun onAdded(added: Edit) { updatePins() } - override fun onSynced(synced: Edit) {} - override fun onDeleted(deleted: List) { updatePins() } - override fun onInvalidated() { updatePins() } - } - - override fun onStart(owner: LifecycleOwner) { - super.onStart(owner) - isStarted = true - show() - } - - override fun onStop(owner: LifecycleOwner) { - super.onStop(owner) - isStarted = false - hide() - } - - override fun onDestroy(owner: LifecycleOwner) { - viewLifecycleScope.cancel() - } - - private fun show() { - if (!isStarted || !isVisible) return - updatePins() - editHistorySource.addListener(editHistoryListener) - } - - private fun hide() { - viewLifecycleScope.coroutineContext.cancelChildren() - viewLifecycleScope.launch(Dispatchers.Main) { pinsMapComponent.clear() } - editHistorySource.removeListener(editHistoryListener) - } - - fun getEditKey(properties: Map): EditKey? = - properties.toEditKey() - - private fun updatePins() { - if (!isVisible) return - viewLifecycleScope.launch { - val edits = withContext(Dispatchers.IO) { editHistorySource.getAll() } - val pins = createEditPins(edits) - pinsMapComponent.set(pins) - } - } - - private fun createEditPins(edits: List): List = - edits.mapIndexed { index, edit -> - Pin( - edit.position, - edit.icon!!.toAndroidResourceId()!!, - edit.toProperties(), - index // most recent first - ) - } -} - -private const val MARKER_EDIT_TYPE = "edit_type" - -private const val MARKER_ELEMENT_TYPE = "element_type" -private const val MARKER_ELEMENT_ID = "element_id" -private const val MARKER_QUEST_TYPE = "quest_type" -private const val MARKER_NOTE_ID = "note_id" -private const val MARKER_ID = "id" - -private const val EDIT_TYPE_ELEMENT = "element" -private const val EDIT_TYPE_NOTE = "note" -private const val EDIT_TYPE_HIDE_OSM_NOTE_QUEST = "hide_osm_note_quest" -private const val EDIT_TYPE_HIDE_OSM_QUEST = "hide_osm_quest" - -private fun Edit.toProperties(): List> = when (this) { - is ElementEdit -> listOf( - MARKER_EDIT_TYPE to EDIT_TYPE_ELEMENT, - MARKER_ID to id.toString() - ) - is NoteEdit -> listOf( - MARKER_EDIT_TYPE to EDIT_TYPE_NOTE, - MARKER_ID to id.toString() - ) - is OsmNoteQuestHidden -> listOf( - MARKER_EDIT_TYPE to EDIT_TYPE_HIDE_OSM_NOTE_QUEST, - MARKER_NOTE_ID to note.id.toString() - ) - is OsmQuestHidden -> listOf( - MARKER_EDIT_TYPE to EDIT_TYPE_HIDE_OSM_QUEST, - MARKER_ELEMENT_TYPE to elementType.name, - MARKER_ELEMENT_ID to elementId.toString(), - MARKER_QUEST_TYPE to questType.name - ) - else -> throw IllegalArgumentException() -} - -private fun Map.toEditKey(): EditKey? = when (get(MARKER_EDIT_TYPE)) { - EDIT_TYPE_ELEMENT -> - ElementEditKey(getValue(MARKER_ID).toLong()) - EDIT_TYPE_NOTE -> - NoteEditKey(getValue(MARKER_ID).toLong()) - EDIT_TYPE_HIDE_OSM_QUEST -> - QuestHiddenKey(OsmQuestKey( - ElementType.valueOf(getValue(MARKER_ELEMENT_TYPE)), - getValue(MARKER_ELEMENT_ID).toLong(), - getValue(MARKER_QUEST_TYPE) - )) - EDIT_TYPE_HIDE_OSM_NOTE_QUEST -> - QuestHiddenKey(OsmNoteQuestKey(getValue(MARKER_NOTE_ID).toLong())) - else -> null -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapFragment.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapFragment.kt deleted file mode 100644 index 64b0593224f..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapFragment.kt +++ /dev/null @@ -1,587 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map - -import android.content.Context -import android.graphics.PointF -import android.os.Bundle -import androidx.annotation.DrawableRes -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesSource -import de.westnordost.streetcomplete.data.edithistory.EditHistorySource -import de.westnordost.streetcomplete.data.edithistory.EditKey -import de.westnordost.streetcomplete.data.location.SurveyChecker -import de.westnordost.streetcomplete.data.osm.edits.MapDataWithEditsSource -import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.data.osmtracks.Trackpoint -import de.westnordost.streetcomplete.data.overlays.SelectedOverlaySource -import de.westnordost.streetcomplete.data.preferences.Preferences -import de.westnordost.streetcomplete.data.quest.QuestKey -import de.westnordost.streetcomplete.data.quest.QuestTypeRegistry -import de.westnordost.streetcomplete.data.quest.VisibleQuestsSource -import de.westnordost.streetcomplete.data.visiblequests.QuestTypeOrderSource -import de.westnordost.streetcomplete.screens.main.map.components.CurrentLocationMapComponent -import de.westnordost.streetcomplete.screens.main.map.components.DownloadedAreaMapComponent -import de.westnordost.streetcomplete.screens.main.map.components.FocusGeometryMapComponent -import de.westnordost.streetcomplete.screens.main.map.components.GeometryMarkersMapComponent -import de.westnordost.streetcomplete.screens.main.map.components.PinsMapComponent -import de.westnordost.streetcomplete.screens.main.map.components.SelectedPinsMapComponent -import de.westnordost.streetcomplete.screens.main.map.components.StyleableOverlayMapComponent -import de.westnordost.streetcomplete.screens.main.map.components.TracksMapComponent -import de.westnordost.streetcomplete.screens.main.map.maplibre.CameraPosition -import de.westnordost.streetcomplete.screens.main.map.maplibre.MapImages -import de.westnordost.streetcomplete.screens.main.map.maplibre.Padding -import de.westnordost.streetcomplete.screens.main.map.maplibre.camera -import de.westnordost.streetcomplete.screens.main.map.maplibre.toLatLon -import de.westnordost.streetcomplete.ui.common.quest.Marker -import de.westnordost.streetcomplete.util.ktx.dpToPx -import de.westnordost.streetcomplete.util.ktx.toLatLon -import de.westnordost.streetcomplete.util.ktx.toLocation -import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json -import org.koin.android.ext.android.inject -import org.maplibre.android.geometry.LatLng -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.Style -import org.maplibre.android.style.layers.Property -import org.maplibre.android.style.layers.PropertyFactory.visibility -import org.maplibre.compose.location.AndroidHeadingProvider -import org.maplibre.compose.location.HeadingMeasurement -import org.maplibre.compose.location.HeadingProvider -import org.maplibre.compose.location.HeadingRequest -import org.maplibre.compose.location.LocationEvent -import org.maplibre.compose.location.LocationMeasurement -import org.maplibre.spatialk.units.Bearing -import org.maplibre.spatialk.units.International -import org.maplibre.spatialk.units.extensions.degrees -import org.maplibre.spatialk.units.extensions.inDegrees -import kotlin.time.Duration.Companion.milliseconds - -/** This is the map shown in the main view. It manages a map that shows the quest pins, quest - * geometry, overlays, tracks, location... */ -class MainMapFragment : MapFragment() { - - private val questTypeOrderSource: QuestTypeOrderSource by inject() - private val questTypeRegistry: QuestTypeRegistry by inject() - private val visibleQuestsSource: VisibleQuestsSource by inject() - private val editHistorySource: EditHistorySource by inject() - private val mapDataSource: MapDataWithEditsSource by inject() - private val selectedOverlaySource: SelectedOverlaySource by inject() - private val downloadedTilesSource: DownloadedTilesSource by inject() - private val surveyChecker: SurveyChecker by inject() - private val prefs: Preferences by inject() - - private lateinit var headingProvider: HeadingProvider - - private var mapImages: MapImages? = null - private var geometryMarkersMapComponent: GeometryMarkersMapComponent? = null - private var pinsMapComponent: PinsMapComponent? = null - private var selectedPinsMapComponent: SelectedPinsMapComponent? = null - private var geometryMapComponent: FocusGeometryMapComponent? = null - private var questPinsManager: QuestPinsManager? = null - private var editHistoryPinsManager: EditHistoryPinsManager? = null - private var styleableOverlayMapComponent: StyleableOverlayMapComponent? = null - private var styleableOverlayManager: StyleableOverlayManager? = null - private var downloadedAreaMapComponent: DownloadedAreaMapComponent? = null - private var downloadedAreaManager: DownloadedAreaManager? = null - private var locationMapComponent: CurrentLocationMapComponent? = null - private var tracksMapComponent: TracksMapComponent? = null - - interface Listener { - fun onClickedQuest(questKey: QuestKey) - fun onClickedEdit(editKey: EditKey) - fun onClickedElement(elementKey: ElementKey) - fun onClickedMapAt(position: LatLon, clickAreaSizeInMeters: Double) - - /** Called after the map fragment updated its displayed location */ - fun onDisplayedLocationDidChange() - } - private val listener: Listener? get() = parentFragment as? Listener ?: activity as? Listener - - /** When the view follows the GPS position, whether the view already zoomed to the location once*/ - private var zoomedYet = false - - /** The GPS position at which the user is displayed at */ - var displayedLocation: LocationMeasurement? = null - private set - - /** The GPS trackpoints the user has walked */ - private var tracks: ArrayList> - - private var _recordedTracks: ArrayList - /** The GPS trackpoints the user has recorded */ - val recordedTracks: List get() = _recordedTracks - - /** If we are actively recording track history */ - var isRecordingTracks = false - private set - - /** Whether the view should automatically center on the GPS location */ - var isFollowingPosition = true - set(value) { - if (!value) zoomedYet = false - field = value - } - - /** Whether the view should automatically rotate with bearing (like during navigation) */ - var isNavigationMode: Boolean = false - set(value) { - val valueChanged = field != value - field = value - if (valueChanged) onUpdatedNavigationMode() - } - - enum class PinMode { NONE, QUESTS, EDITS } - var pinMode: PinMode = PinMode.QUESTS - set(value) { - if (field == value) return - field = value - onUpdatedPinMode() - } - - private var previouslyHiddenLayers: List = emptyList() - - private val overlayListener = object : SelectedOverlaySource.Listener { - override fun onSelectedOverlayChanged() { - this@MainMapFragment.onSelectedOverlayChanged() - } - } - - //region Lifecycle - - init { - tracks = ArrayList() - tracks.add(ArrayList()) - _recordedTracks = ArrayList() - } - - override fun onAttach(context: Context) { - super.onAttach(context) - headingProvider = AndroidHeadingProvider(context) - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - headingProvider.updates(HeadingRequest(33.milliseconds)).collect { heading -> - onCompassRotationChanged(heading) - } - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState != null) { - displayedLocation = savedInstanceState.getString(DISPLAYED_LOCATION)?.let { Json.decodeFromString(it) } - isRecordingTracks = savedInstanceState.getBoolean(TRACKS_IS_RECORDING) - tracks = Json.decodeFromString(savedInstanceState.getString(TRACKS)!!) - } - } - - override suspend fun onMapStyleLoaded(map: MapLibreMap, style: Style) { - setupComponents(requireContext(), map, style) - - map.addOnMapClickListener(::onClickMap) - - setupLayers(style) - - setupData(map) - } - - private fun setupComponents(context: Context, map: MapLibreMap, style: Style) { - val fingerRadius = context.resources.dpToPx(CLICK_AREA_SIZE_IN_DP / 2) - - mapImages = MapImages(context.resources, style) - - geometryMarkersMapComponent = GeometryMarkersMapComponent(context, map, mapImages!!) - - locationMapComponent = CurrentLocationMapComponent(context, style, map) - viewLifecycleOwner.lifecycle.addObserver(locationMapComponent!!) - - tracksMapComponent = TracksMapComponent(context, style, map) - viewLifecycleOwner.lifecycle.addObserver(tracksMapComponent!!) - - pinsMapComponent = PinsMapComponent(context, context.contentResolver, map, mapImages!!, ::onClickPin) - geometryMapComponent = FocusGeometryMapComponent(context.contentResolver, map) - viewLifecycleOwner.lifecycle.addObserver(geometryMapComponent!!) - - styleableOverlayMapComponent = StyleableOverlayMapComponent(context, map, mapImages!!, fingerRadius, ::onClickElement) - - downloadedAreaMapComponent = DownloadedAreaMapComponent(context, map) - - selectedPinsMapComponent = SelectedPinsMapComponent(context, map, mapImages!!) - viewLifecycleOwner.lifecycle.addObserver(selectedPinsMapComponent!!) - } - - private fun setupLayers(style: Style) { - // layers added first appear behind other layers - - // left-and-right lines should be rendered behind the actual road - val firstCasingLayer = "pedestrian-tunnel-casing" - for (layer in styleableOverlayMapComponent?.sideLayers.orEmpty()) { - style.addLayerBelow(layer, firstCasingLayer) - } - val firstBridgeCasingLayer = "pedestrian-bridge-casing" - for (layer in styleableOverlayMapComponent?.sideLayersBridge.orEmpty()) { - style.addLayerBelow(layer, firstBridgeCasingLayer) - } - - // labels should be on top of other layers - val firstLabelLayer = "labels-country" - for (layer in listOfNotNull( - downloadedAreaMapComponent?.layers, - styleableOverlayMapComponent?.layers, - tracksMapComponent?.layers, - ).flatten()) { - style.addLayerBelow(layer, firstLabelLayer) - } - - // these are always on top of everything else (including labels) - for (layer in listOfNotNull( - styleableOverlayMapComponent?.labelLayers, - geometryMarkersMapComponent?.layers, - geometryMapComponent?.layers, - locationMapComponent?.layers, - pinsMapComponent?.layers, - selectedPinsMapComponent?.layers - ).flatten()) { - style.addLayer(layer) - } - } - - private fun setupData(map: MapLibreMap) { - restoreMapState() - centerCurrentPositionIfFollowing() - - questPinsManager = QuestPinsManager(map, pinsMapComponent!!, questTypeOrderSource, questTypeRegistry, visibleQuestsSource) - questPinsManager!!.isVisible = pinMode == PinMode.QUESTS - viewLifecycleOwner.lifecycle.addObserver(questPinsManager!!) - - editHistoryPinsManager = EditHistoryPinsManager(pinsMapComponent!!, editHistorySource) - editHistoryPinsManager!!.isVisible = pinMode == PinMode.EDITS - viewLifecycleOwner.lifecycle.addObserver(editHistoryPinsManager!!) - - styleableOverlayManager = StyleableOverlayManager(map, styleableOverlayMapComponent!!, mapDataSource, selectedOverlaySource) - viewLifecycleOwner.lifecycle.addObserver(styleableOverlayManager!!) - - downloadedAreaManager = DownloadedAreaManager(downloadedAreaMapComponent!!, downloadedTilesSource) - viewLifecycleOwner.lifecycle.addObserver(downloadedAreaManager!!) - - onSelectedOverlayChanged() - selectedOverlaySource.addListener(overlayListener) - - locationMapComponent?.targetLocation = displayedLocation - - val positionsLists = tracks.map { track -> track.map { it.position } } - tracksMapComponent?.setTracks(positionsLists, isRecordingTracks) - } - - override fun onStop() { - super.onStop() - saveMapState() - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString(DISPLAYED_LOCATION, Json.encodeToString(displayedLocation)) - // the amount of data one can put into a bundle is limited, let's cut off at 1000 points - outState.putString(TRACKS, Json.encodeToString(tracks.takeLastNested(1000))) - outState.putBoolean(TRACKS_IS_RECORDING, isRecordingTracks) - } - - override fun onDestroyView() { - super.onDestroyView() - selectedOverlaySource.removeListener(overlayListener) - } - - //endregion - - //region Tracking GPS, Rotation, location availability, pin mode, click ... - - private fun onClickPin(properties: Map) { - when (pinMode) { - PinMode.QUESTS -> { - questPinsManager?.getQuestKey(properties)?.let { listener?.onClickedQuest(it) } - } - PinMode.EDITS -> { - editHistoryPinsManager?.getEditKey(properties)?.let { listener?.onClickedEdit(it) } - } - PinMode.NONE -> {} - } - } - - private fun onClickElement(key: ElementKey) { - listener?.onClickedElement(key) - } - - private fun onClickMap(position: LatLng): Boolean { - val fingerRadius = context?.resources?.dpToPx(CLICK_AREA_SIZE_IN_DP / 2) ?: return false - val clickPos = map?.projection?.toScreenLocation(position) ?: return false - - // no feature: just click the map - val fingerEdgePosition = map?.projection?.fromScreenLocation(PointF(clickPos.x + fingerRadius, clickPos.y)) ?: return false - val fingerRadiusInMeters = position.distanceTo(fingerEdgePosition) - listener?.onClickedMapAt(position.toLatLon(), fingerRadiusInMeters) - return true - } - - private fun onCompassRotationChanged(heading: HeadingMeasurement) { - val cameraBearing = Bearing.North + (map?.camera?.rotation ?: 0.0).degrees - locationMapComponent?.targetRotation = (heading.bearing - cameraBearing).inDegrees.toFloat() - } - - fun onLocationEvent(locationEvent: LocationEvent) { - when (locationEvent) { - is LocationEvent.Update -> { - val location = locationEvent.measurement - displayedLocation = location - surveyChecker.addRecentLocation(locationEvent.toLocation()) - locationMapComponent?.targetLocation = location - addTrackLocation(location) - centerCurrentPositionIfFollowing() - } - is LocationEvent.Unavailable -> { - locationMapComponent?.targetLocation = null - displayedLocation = null - isNavigationMode = false - - tracks = ArrayList() - tracks.add(ArrayList()) - tracksMapComponent?.clear() - } - } - listener?.onDisplayedLocationDidChange() - } - - override fun onMapIsChanging(camera: CameraPosition) { - super.onMapIsChanging(camera) - questPinsManager?.onNewScreenPosition() - styleableOverlayManager?.onNewScreenPosition() - } - - private fun onUpdatedPinMode() { - /* both managers use the same resource (PinsMapComponent), so the newly visible manager - may only be activated after the old has been deactivated - */ - when (pinMode) { - PinMode.QUESTS -> { - editHistoryPinsManager?.isVisible = false - questPinsManager?.isVisible = true - } - PinMode.EDITS -> { - questPinsManager?.isVisible = false - editHistoryPinsManager?.isVisible = true - } - else -> { - questPinsManager?.isVisible = false - editHistoryPinsManager?.isVisible = false - } - } - } - - private fun onSelectedOverlayChanged() { - val new = selectedOverlaySource.selectedOverlay?.hidesLayers.orEmpty() - val old = previouslyHiddenLayers - if (old == new) return - - viewLifecycleScope.launch { - old.forEach { layer -> - map?.style?.getLayer(layer)?.setProperties(visibility(Property.VISIBLE)) - } - new.forEach { layer -> - map?.style?.getLayer(layer)?.setProperties(visibility(Property.NONE)) - } - } - - previouslyHiddenLayers = new - } - - private fun addTrackLocation(location: LocationMeasurement) { - // ignore if too imprecise - val accuracy = location.horizontalAccuracy?.toFloat(International.Meters) - if (accuracy != null && accuracy > MIN_TRACK_ACCURACY) return - val lastLocation = tracks.last().lastOrNull() - - // create new track if last position too old - if (lastLocation != null && !isRecordingTracks) { - if (location.measuredAt.toEpochMilliseconds() - lastLocation.time > MAX_TIME_BETWEEN_LOCATIONS) { - tracks.add(ArrayList()) - tracksMapComponent?.startNewTrack(false) - } - } - val trackpoint = Trackpoint( - position = location.position.toLatLon(), - time = location.measuredAt.toEpochMilliseconds(), - accuracy = accuracy ?: 0f, - elevation = location.position.altitude?.toFloat() ?: 0f - ) - - tracks.last().add(trackpoint) - // in rare cases, onLocationChanged may already be called before the view has been created - // so we need to check that first - if (view != null) { - tracksMapComponent?.addToCurrentTrack(trackpoint.position) - } - } - - //endregion - - //region Control focusing on and highlighting edit / quest / element - - /** Focus the view on the given geometry */ - fun startFocus(geometry: ElementGeometry, padding: Padding?) { - geometryMapComponent?.beginFocusGeometry(geometry, padding) - } - - /** End the focussing but do not return to position before focussing */ - fun clearFocus() { - geometryMapComponent?.clearFocusGeometry() - } - - /** return to the position before focussing */ - fun endFocus() { - geometryMapComponent?.endFocusGeometry() - } - - fun highlightPins(@DrawableRes iconResId: Int, pinPositions: Collection) { - viewLifecycleScope.launch(Dispatchers.Default) { - selectedPinsMapComponent?.set(iconResId, pinPositions) - } - } - - fun hideNonHighlightedPins(questKey: QuestKey? = null) { - pinsMapComponent?.setVisible(false) - } - - fun hideOverlay() { - styleableOverlayMapComponent?.setVisible(false) - } - - fun highlightGeometry(geometry: ElementGeometry) { - geometryMapComponent?.showGeometry(geometry) - } - - /** Clear all highlighting */ - fun clearHighlighting() { - pinsMapComponent?.setVisible(true) - styleableOverlayMapComponent?.setVisible(true) - geometryMapComponent?.clearGeometry() - geometryMarkersMapComponent?.clear() - selectedPinsMapComponent?.clear() - } - - fun clearSelectedPins() { - selectedPinsMapComponent?.clear() - } - - fun setMarkersForCurrentHighlighting(markers: Iterable) { - viewLifecycleScope.launch(Dispatchers.Default) { - geometryMarkersMapComponent?.setAll(markers) - } - } - - //endregion - - //region Control position tracking - - fun startPositionTrackRecording() { - isRecordingTracks = true - _recordedTracks.clear() - tracks.add(ArrayList()) - tracksMapComponent?.startNewTrack(true) - } - - fun stopPositionTrackRecording() { - isRecordingTracks = false - _recordedTracks.clear() - _recordedTracks.addAll(tracks.last()) - tracks.add(ArrayList()) - tracksMapComponent?.startNewTrack(false) - } - - private fun centerCurrentPosition() { - val displayedPosition = displayedLocation?.position?.let { it.toLatLon() } ?: return - - updateCameraPosition(600) { - if (isNavigationMode) { - val bearing = getTrackBearing(tracks.last()) - if (bearing != null) rotation = bearing - tilt = 60.0 - } - - position = displayedPosition - - if (!zoomedYet) { - zoomedYet = true - val currentZoom = map?.camera?.zoom - if (currentZoom != null && currentZoom < 17.0f) zoom = 18.0 - } - } - } - - fun centerCurrentPositionIfFollowing() { - if (isFollowingPosition) centerCurrentPosition() - } - - private fun onUpdatedNavigationMode() { - if (!isNavigationMode) { - // don't reset the rotation to 0 here, because this behavior is useful (#5886) - // (users can always reset it by tapping on the compass) - updateCameraPosition(300) { tilt = 0.0 } - } else { - centerCurrentPositionIfFollowing() - } - } - - //endregion - - //region Save and restore state - - override fun setInitialCameraPosition(camera: CameraPosition) { - super.setInitialCameraPosition(camera) - isFollowingPosition = false - isNavigationMode = false - if (map == null) { - saveMapState() - } - } - - private fun restoreMapState() { - isFollowingPosition = prefs.mapIsFollowing - isNavigationMode = prefs.mapIsNavigationMode - } - - private fun saveMapState() { - prefs.mapIsFollowing = isFollowingPosition - prefs.mapIsNavigationMode = isNavigationMode - } - - //endregion - - companion object { - private const val DISPLAYED_LOCATION = "displayed_location" - private const val TRACKS = "tracks" - private const val TRACKS_IS_RECORDING = "tracks_is_recording" - - private const val MIN_TRACK_ACCURACY = 20f - private const val MAX_TIME_BETWEEN_LOCATIONS = 60L * 1000 // 1 minute - - private const val CLICK_AREA_SIZE_IN_DP = 28 - } -} - -private fun ArrayList>.takeLastNested(n: Int): ArrayList> { - var sum = 0 - for (i in lastIndex downTo 0) { - val s = get(i).size - if (sum + s > n) { - val result = ArrayList(subList(i + 1, size)) - if (n > sum) result.add(0, ArrayList(get(i).takeLast(n - sum))) - return result - } - sum += s - } - return this -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapFragment.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapFragment.kt deleted file mode 100644 index cfe38c04bc9..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapFragment.kt +++ /dev/null @@ -1,240 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map - -import android.graphics.PointF -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import de.westnordost.streetcomplete.ApplicationConstants -import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.data.preferences.Preferences -import de.westnordost.streetcomplete.screens.main.map.components.SceneMapComponent -import de.westnordost.streetcomplete.screens.main.map.maplibre.CameraPosition -import de.westnordost.streetcomplete.screens.main.map.maplibre.CameraUpdate -import de.westnordost.streetcomplete.screens.main.map.maplibre.awaitGetMap -import de.westnordost.streetcomplete.screens.main.map.maplibre.camera -import de.westnordost.streetcomplete.screens.main.map.maplibre.deleteRegionsOlderThan -import de.westnordost.streetcomplete.screens.main.map.maplibre.getMetersPerPixel -import de.westnordost.streetcomplete.screens.main.map.maplibre.screenAreaToBoundingBox -import de.westnordost.streetcomplete.screens.main.map.maplibre.toLatLng -import de.westnordost.streetcomplete.screens.main.map.maplibre.toLatLon -import de.westnordost.streetcomplete.screens.main.map.maplibre.updateCamera -import de.westnordost.streetcomplete.util.ktx.dpToPx -import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds -import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject -import org.maplibre.android.MapLibre -import org.maplibre.android.gestures.MoveGestureDetector -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.MapView -import org.maplibre.android.maps.Style -import org.maplibre.android.offline.OfflineManager - -/** Manages a map that remembers its last location */ -open class MapFragment : Fragment() { - - protected var map: MapLibreMap? = null - private lateinit var mapView: MapView - private var sceneMapComponent: SceneMapComponent? = null - - private val prefs: Preferences by inject() - - interface Listener { - /** Called when the map has been completely initialized */ - fun onMapInitialized() - /** Called during camera animation and while the map is being controlled by a user */ - fun onMapIsChanging(camera: CameraPosition) - /** Called when the user begins to pan the map */ - fun onPanBegin() - /** Called when the user long-presses the map */ - fun onLongPress(point: PointF, position: LatLon) - /** Called when user starts to move the map */ - fun onUserCameraMoveStarted() - } - private val listener: Listener? get() = parentFragment as? Listener ?: activity as? Listener - - // Note: offline regions may exceed this limit, but will count against it - // This means that when offline regions exceed size, no tiles will be cached when panning - // This means that when offline regions exceed size, no tiles will be cached when panning - - /* ------------------------------------ Lifecycle ------------------------------------------- */ - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - MapLibre.getInstance(requireContext()) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - MapView(inflater.context).also { mapView = it } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - mapView.onCreate(savedInstanceState) - mapView.foreground = view.context.getDrawable(R.color.background) - - cleanOldOfflineRegions() - - viewLifecycleScope.launch { - val map = mapView.awaitGetMap() - this@MapFragment.map = map - initMap(map) - } - } - - private fun cleanOldOfflineRegions() { - // the offline manager is only available together with the map, i.e. not from the CleanerWorker - lifecycleScope.launch { - delay(30000) // cleaning is low priority, do it once startup is done - val oldDataTimestamp = nowAsEpochMilliseconds() - ApplicationConstants.DELETE_OLD_DATA_AFTER - OfflineManager.getInstance(requireContext()).deleteRegionsOlderThan(oldDataTimestamp) - } - } - - override fun onStart() { - super.onStart() - // sceneMapComponent might actually be null if map style not initialized yet - sceneMapComponent?.updateStyle() - } - - override fun onResume() { - super.onResume() - mapView.onResume() - } - - override fun onPause() { - super.onPause() - mapView.onPause() - } - - override fun onStop() { - super.onStop() - saveMapState() - } - - override fun onDestroyView() { - super.onDestroyView() - map = null - mapView.onDestroy() - } - - override fun onLowMemory() { - super.onLowMemory() - mapView.onLowMemory() - } - - /* ------------------------------------------- Map ----------------------------------------- */ - - private suspend fun initMap(map: MapLibreMap) { - map.uiSettings.isCompassEnabled = false - map.uiSettings.isAttributionEnabled = false - map.uiSettings.isLogoEnabled = false - map.uiSettings.flingThreshold = 250 - map.uiSettings.flingAnimationBaseTime = 500 - map.uiSettings.isDisableRotateWhenScaling = true - // workaround for https://github.com/maplibre/maplibre-native/issues/2792 - map.gesturesManager.moveGestureDetector.moveThreshold = resources.dpToPx(5f) - map.gesturesManager.rotateGestureDetector.angleThreshold = 1.5f - map.gesturesManager.shoveGestureDetector.pixelDeltaThreshold = resources.dpToPx(8f) - - map.addOnMoveListener(object : MapLibreMap.OnMoveListener { - override fun onMoveBegin(detector: MoveGestureDetector) { listener?.onPanBegin() } - override fun onMove(detector: MoveGestureDetector) {} - override fun onMoveEnd(detector: MoveGestureDetector) {} - }) - map.addOnCameraMoveListener { - val camera = cameraPosition ?: return@addOnCameraMoveListener - onMapIsChanging(camera) - listener?.onMapIsChanging(camera) - } - map.addOnCameraMoveStartedListener { reason -> - if (reason == MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE) { - listener?.onUserCameraMoveStarted() - } - } - map.addOnMapLongClickListener { pos -> - onLongPress(map.projection.toScreenLocation(pos), pos.toLatLon()) - true - } - - val sceneMapComponent = SceneMapComponent(requireContext(), map) - val style = sceneMapComponent.loadStyle() - this.sceneMapComponent = sceneMapComponent - - restoreMapState() - mapView.foreground = null - - onMapStyleLoaded(map, style) - - listener?.onMapInitialized() - } - - /* ----------------------------- Overridable map callbacks --------------------------------- */ - - protected open suspend fun onMapStyleLoaded(map: MapLibreMap, style: Style) {} - - protected open fun onMapIsChanging(camera: CameraPosition) {} - - /* ---------------------- Overridable callbacks for map interaction ------------------------ */ - - open fun onLongPress(point: PointF, position: LatLon) { - listener?.onLongPress(point, position) - } - - /* -------------------------------- Save and Restore State ---------------------------------- */ - - private fun restoreMapState() { - map?.camera = loadCameraPosition() - } - - private fun saveMapState() { - val camera = map?.camera ?: return - saveCameraPosition(camera) - } - - private fun loadCameraPosition() = CameraPosition( - position = prefs.mapPosition, - rotation = prefs.mapRotation, - tilt = prefs.mapTilt, - zoom = prefs.mapZoom - ) - - private fun saveCameraPosition(camera: CameraPosition) { - prefs.mapRotation = camera.rotation - prefs.mapTilt = camera.tilt - prefs.mapZoom = camera.zoom - prefs.mapPosition = camera.position - } - - /* ------------------------------- Controlling the map -------------------------------------- */ - - fun getPositionAt(point: PointF): LatLon? = - map?.projection?.fromScreenLocation(point)?.toLatLon() - - fun getPointOf(pos: LatLon): PointF? = - map?.projection?.toScreenLocation(pos.toLatLng()) - - val cameraPosition: CameraPosition? - get() = map?.camera - - fun updateCameraPosition(duration: Int = 0, builder: CameraUpdate.() -> Unit) { - map?.updateCamera(duration, requireContext().contentResolver, builder) - } - - open fun setInitialCameraPosition(camera: CameraPosition) { - if (map != null) { - map?.camera = camera - } else { - saveCameraPosition(camera) - } - } - - fun getDisplayedArea(): BoundingBox? = map?.screenAreaToBoundingBox() - - fun getMetersPerPixel(): Double? = map?.getMetersPerPixel() -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapIconBitmapCreator.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapIconBitmapCreator.kt deleted file mode 100644 index 511480e9f58..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapIconBitmapCreator.kt +++ /dev/null @@ -1,68 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.RectF -import androidx.annotation.DrawableRes -import androidx.core.graphics.toRect -import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.util.ktx.createBitmap -import de.westnordost.streetcomplete.util.ktx.dpToPx -import de.westnordost.streetcomplete.util.ktx.toSdf -import kotlin.math.ceil - -fun createPinBitmap( - context: Context, - @DrawableRes iconResId: Int -): Bitmap { - val scale = 1f - val size = context.resources.dpToPx(71 * scale) - val sizeInt = ceil(size).toInt() - val iconSize = context.resources.dpToPx(48 * scale) - val iconPinOffset = context.resources.dpToPx(2 * scale) - val pinTopRightPadding = context.resources.dpToPx(5 * scale) - - val pin = context.getDrawable(R.drawable.pin)!! - val pinShadow = context.getDrawable(R.drawable.pin_shadow)!! - - val pinWidth = (size - pinTopRightPadding) * pin.intrinsicWidth / pin.intrinsicHeight - val pinXOffset = size - pinTopRightPadding - pinWidth - - val bitmap = Bitmap.createBitmap(sizeInt, sizeInt, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - pinShadow.setBounds(0, 0, sizeInt, sizeInt) - pinShadow.draw(canvas) - pin.bounds = RectF( - pinXOffset, - pinTopRightPadding, - size - pinTopRightPadding, - size - ).toRect() - pin.draw(canvas) - val questIcon = context.getDrawable(iconResId)!! - questIcon.bounds = RectF( - pinXOffset + iconPinOffset, - pinTopRightPadding + iconPinOffset, - pinXOffset + iconPinOffset + iconSize, - pinTopRightPadding + iconPinOffset + iconSize - ).toRect() - questIcon.draw(canvas) - return bitmap -} - -fun createIconBitmap( - context: Context, - @DrawableRes iconResId: Int, - createSdf: Boolean = false, - maxSizeDp: Int = 48 -): Bitmap { - val drawable = context.getDrawable(iconResId)!! - val maxIconSize = context.resources.dpToPx(maxSizeDp).toInt() - val bitmap = drawable.createBitmap( - width = drawable.intrinsicWidth.coerceAtMost(maxIconSize), - height = drawable.intrinsicHeight.coerceAtMost(maxIconSize), - ) - if (!createSdf) return bitmap - return bitmap.toSdf(radius = context.resources.dpToPx(8.0).toDouble()) -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/QuestPinsManager.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/QuestPinsManager.kt deleted file mode 100644 index 35736097576..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/QuestPinsManager.kt +++ /dev/null @@ -1,287 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map - -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import de.westnordost.streetcomplete.data.download.tiles.TilesRect -import de.westnordost.streetcomplete.data.download.tiles.enclosingTilesRect -import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox -import de.westnordost.streetcomplete.data.osm.mapdata.ElementType -import de.westnordost.streetcomplete.data.quest.OsmNoteQuestKey -import de.westnordost.streetcomplete.data.quest.OsmQuestKey -import de.westnordost.streetcomplete.data.quest.Quest -import de.westnordost.streetcomplete.data.quest.QuestKey -import de.westnordost.streetcomplete.data.quest.QuestType -import de.westnordost.streetcomplete.data.quest.QuestTypeRegistry -import de.westnordost.streetcomplete.data.quest.VisibleQuestsSource -import de.westnordost.streetcomplete.data.visiblequests.QuestTypeOrderSource -import de.westnordost.streetcomplete.screens.main.map.components.Pin -import de.westnordost.streetcomplete.screens.main.map.components.PinsMapComponent -import de.westnordost.streetcomplete.screens.main.map.maplibre.screenAreaToBoundingBox -import de.westnordost.streetcomplete.util.math.contains -import de.westnordost.streetcomplete.view.toAndroidResourceId -import kotlinx.atomicfu.locks.ReentrantLock -import kotlinx.atomicfu.locks.withLock -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.maplibre.android.maps.MapLibreMap - -/** Manages the layer of quest pins in the map view: - * Gets told by the QuestsMapFragment when a new area is in view and independently pulls the quests - * for the bbox surrounding the area from database and holds it in memory. */ -class QuestPinsManager( - private val map: MapLibreMap, - private val pinsMapComponent: PinsMapComponent, - private val questTypeOrderSource: QuestTypeOrderSource, - private val questTypeRegistry: QuestTypeRegistry, - private val visibleQuestsSource: VisibleQuestsSource -) : DefaultLifecycleObserver { - - // draw order in which the quest types should be rendered on the map - private val questTypeOrdersLock = ReentrantLock() - private val questTypeOrders: MutableMap = mutableMapOf() - // last displayed rect of (zoom 16) tiles - private var lastDisplayedRect: TilesRect? = null - // quests in current view: key -> [pin, ...] - private val questsInView: MutableMap> = mutableMapOf() - private val questsInViewMutex = Mutex() - - private val visibleQuestsSourceMutex = Mutex() - - private val viewLifecycleScope: CoroutineScope = CoroutineScope(SupervisorJob()) - - private var updateJob: Job? = null - - /** Switch visibility of quest pins layer */ - var isVisible: Boolean = false - set(value) { - if (field == value) return - field = value - if (value) show() else hide() - } - - private var isStarted: Boolean = false - - private val visibleQuestsListener = object : VisibleQuestsSource.Listener { - override fun onUpdated(added: Collection, removed: Collection) { - val oldUpdateJob = updateJob - updateJob = viewLifecycleScope.launch { - oldUpdateJob?.join() // don't cancel, as updateQuestPins only updates existing data - updateQuestPins(added, removed) - } - } - - override fun onInvalidated() { - invalidate() - } - } - - private val questTypeOrderListener = object : QuestTypeOrderSource.Listener { - override fun onQuestTypeOrderAdded(item: QuestType, toAfter: QuestType) { - reinitializeQuestTypeOrders() - } - - override fun onQuestTypeOrdersChanged() { - reinitializeQuestTypeOrders() - } - } - - override fun onStart(owner: LifecycleOwner) { - super.onStart(owner) - isStarted = true - show() - } - - override fun onStop(owner: LifecycleOwner) { - super.onStop(owner) - isStarted = false - hide() - } - - override fun onDestroy(owner: LifecycleOwner) { - viewLifecycleScope.cancel() - } - - private fun show() { - if (!isStarted || !isVisible) return - initializeQuestTypeOrders() - onNewScreenPosition() - visibleQuestsSource.addListener(visibleQuestsListener) - questTypeOrderSource.addListener(questTypeOrderListener) - } - - private fun hide() { - viewLifecycleScope.coroutineContext.cancelChildren() - clear() - visibleQuestsSource.removeListener(visibleQuestsListener) - questTypeOrderSource.removeListener(questTypeOrderListener) - } - - private fun invalidate() { - lastDisplayedRect = null - onNewScreenPosition() - } - - private fun clear() { - lastDisplayedRect = null - viewLifecycleScope.launch { - questsInViewMutex.withLock { questsInView.clear() } - withContext(Dispatchers.Main) { pinsMapComponent.clear() } - } - } - - fun getQuestKey(properties: Map): QuestKey? = - properties.toQuestKey() - - fun onNewScreenPosition() { - if (!isStarted || !isVisible) return - viewLifecycleScope.launch { updateCurrentScreenArea() } - } - - private suspend fun updateCurrentScreenArea() { - // require zoom >= 14, which is the lowest zoom level where quests are shown - val zoom = map.cameraPosition.zoom - if (zoom < 14) return - val displayedArea = withContext(Dispatchers.Main) { map.screenAreaToBoundingBox() } - val tilesRect = displayedArea.enclosingTilesRect(TILES_ZOOM) - // area too big -> skip (performance) - if (tilesRect.size > 32) return - val isNewRect = lastDisplayedRect?.contains(tilesRect) != true - if (!isNewRect) return - - lastDisplayedRect = tilesRect - /* Imagine you are panning the map fast, many different tiles come into and vanish from view - again quickly. Suppose, that fetching the data from DB takes longer than panning through - and out of a tile - we would end up with a long queue of DB fetches (and subsequent - map updates) of which the data is discarded immediately after because it is out of view - again. - So, what we do here is to discard each such update except the last one. All jobs started - in potentially quick succession have to wait at for the DB fetch to complete and will - stop when they have been cancelled in the meantime. The same with if they have been - cancelled just after the DB fetch etc. (The coroutine can be cancelled at every place - where you see that arrow with that green squiggle in the IDE) - */ - updateJob?.cancel() - updateJob = viewLifecycleScope.launch { - val bbox = tilesRect.asBoundingBox(TILES_ZOOM) - setQuestPins(bbox) - } - } - - private suspend fun setQuestPins(bbox: BoundingBox) { - val quests = visibleQuestsSourceMutex.withLock { - withContext(Dispatchers.IO) { visibleQuestsSource.getAll(bbox) } - } - val pins = questsInViewMutex.withLock { - /* Usually, we would call questsInView.clear() here. However, - quests have only a single position, but may have multiple pins (see - Quest::markerLocations), e.g. at the start and end of a long road. A pin of a quest - whose center is outside the current view may hence be within the current view. Quest - pins like these should not disappear when panning the map. - Therefore, remove all quests that are not in view anymore that ... - */ - questsInView.entries.removeAll { (_, pins) -> - // only have one pin (pin position = quest position) - pins.size == 1 || - // or has no pins in the current view - pins.none { it.position in bbox } - } - quests.forEach { questsInView[it.key] = createQuestPins(it) } - questsInView.values.flatten() - } - pinsMapComponent.set(pins) - } - - private suspend fun updateQuestPins(added: Collection, removed: Collection) { - val pins = questsInViewMutex.withLock { - val displayedBBox = lastDisplayedRect?.asBoundingBox(TILES_ZOOM) ?: return - var hasChanges = false - - removed.forEach { - if (questsInView.remove(it) != null) hasChanges = true - } - added.forEach { - if (displayedBBox.contains(it.position)) { - questsInView[it.key] = createQuestPins(it) - hasChanges = true - } else { - if (questsInView.remove(it.key) != null) hasChanges = true - } - } - - if (!hasChanges) return - - questsInView.values.flatten() - } - pinsMapComponent.set(pins) - } - - private fun initializeQuestTypeOrders() { - // this needs to be reinitialized when the quest order changes - val sortedQuestTypes = questTypeRegistry.toMutableList() - questTypeOrderSource.sort(sortedQuestTypes) - questTypeOrdersLock.withLock { - questTypeOrders.clear() - sortedQuestTypes.forEachIndexed { index, questType -> - questTypeOrders[questType] = index - } - } - } - - private fun createQuestPins(quest: Quest): List { - val props = quest.key.toProperties() - val order = questTypeOrdersLock.withLock { questTypeOrders[quest.type] ?: 0 } - return quest.markerLocations.map { Pin(it, quest.type.icon.toAndroidResourceId()!!, props, order) } - } - - private fun reinitializeQuestTypeOrders() { - initializeQuestTypeOrders() - invalidate() - } - - companion object { - private const val TILES_ZOOM = 16 - } -} - -private const val MARKER_QUEST_GROUP = "quest_group" - -private const val MARKER_ELEMENT_TYPE = "element_type" -private const val MARKER_ELEMENT_ID = "element_id" -private const val MARKER_QUEST_TYPE = "quest_type" -private const val MARKER_NOTE_ID = "note_id" - -private const val QUEST_GROUP_OSM = "osm" -private const val QUEST_GROUP_OSM_NOTE = "osm_note" - -private fun QuestKey.toProperties(): List> = when (this) { - is OsmNoteQuestKey -> listOf( - MARKER_QUEST_GROUP to QUEST_GROUP_OSM_NOTE, - MARKER_NOTE_ID to noteId.toString() - ) - is OsmQuestKey -> listOf( - MARKER_QUEST_GROUP to QUEST_GROUP_OSM, - MARKER_ELEMENT_TYPE to elementType.name, - MARKER_ELEMENT_ID to elementId.toString(), - MARKER_QUEST_TYPE to questTypeName - ) -} - -private fun Map.toQuestKey(): QuestKey? = when (get(MARKER_QUEST_GROUP)) { - QUEST_GROUP_OSM_NOTE -> - OsmNoteQuestKey(getValue(MARKER_NOTE_ID).toLong()) - QUEST_GROUP_OSM -> - OsmQuestKey( - ElementType.valueOf(getValue(MARKER_ELEMENT_TYPE)), - getValue(MARKER_ELEMENT_ID).toLong(), - getValue(MARKER_QUEST_TYPE) - ) - else -> null -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/StyleableOverlayManager.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/StyleableOverlayManager.kt deleted file mode 100644 index 4fee4c6821d..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/StyleableOverlayManager.kt +++ /dev/null @@ -1,220 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map - -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import de.westnordost.streetcomplete.data.download.tiles.TilesRect -import de.westnordost.streetcomplete.data.download.tiles.enclosingTilesRect -import de.westnordost.streetcomplete.data.osm.edits.MapDataWithEditsSource -import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox -import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey -import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.key -import de.westnordost.streetcomplete.data.overlays.Overlay -import de.westnordost.streetcomplete.data.overlays.SelectedOverlaySource -import de.westnordost.streetcomplete.screens.main.map.components.StyleableOverlayMapComponent -import de.westnordost.streetcomplete.screens.main.map.components.StyledElement -import de.westnordost.streetcomplete.screens.main.map.maplibre.screenAreaToBoundingBox -import de.westnordost.streetcomplete.util.math.intersect -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.maplibre.android.maps.MapLibreMap - -/** Manages the layer of styled map data in the map view: - * Gets told by the MainMapFragment when a new area is in view and independently pulls the map - * data for the bbox surrounding the area from database and holds it in memory. */ -class StyleableOverlayManager( - private val map: MapLibreMap, - private val mapComponent: StyleableOverlayMapComponent, - private val mapDataSource: MapDataWithEditsSource, - private val selectedOverlaySource: SelectedOverlaySource -) : DefaultLifecycleObserver { - - // last displayed rect of (zoom 16) tiles - private var lastDisplayedRect: TilesRect? = null - // map data in current view: key -> [pin, ...] - private val mapDataInView: MutableMap = mutableMapOf() - private val mapDataInViewMutex = Mutex() - - private val mapDataSourceMutex = Mutex() - - private val viewLifecycleScope: CoroutineScope = CoroutineScope(SupervisorJob()) - - private var updateJob: Job? = null - - private var overlay: Overlay? = null - set(value) { - if (field == value) return - val wasNull = field == null - val isNullNow = value == null - field = value - when { - isNullNow -> hide() - wasNull -> show() - else -> { - clear() - invalidate() - } - } - } - - private val overlayListener = object : SelectedOverlaySource.Listener { - override fun onSelectedOverlayChanged() { - overlay = selectedOverlaySource.selectedOverlay - } - } - - private val mapDataListener = object : MapDataWithEditsSource.Listener { - override fun onUpdated(updated: MapDataWithGeometry, deleted: Collection) { - val oldUpdateJob = updateJob - updateJob = viewLifecycleScope.launch { - oldUpdateJob?.join() // don't cancel, as updateStyledElements only updates existing data - updateStyledElements(updated, deleted) - } - } - - override fun onReplacedForBBox(bbox: BoundingBox, mapDataWithGeometry: MapDataWithGeometry) { - invalidate() - } - - override fun onCleared() { - clear() - } - } - - override fun onStart(owner: LifecycleOwner) { - super.onStart(owner) - overlay = selectedOverlaySource.selectedOverlay - selectedOverlaySource.addListener(overlayListener) - } - - override fun onStop(owner: LifecycleOwner) { - super.onStop(owner) - overlay = null - selectedOverlaySource.removeListener(overlayListener) - } - - override fun onDestroy(owner: LifecycleOwner) { - viewLifecycleScope.cancel() - } - - private fun show() { - lastDisplayedRect = null - onNewScreenPosition() - mapDataSource.addListener(mapDataListener) - } - - private fun invalidate() { - lastDisplayedRect = null - onNewScreenPosition() - } - - private fun hide() { - viewLifecycleScope.coroutineContext.cancelChildren() - clear() - mapDataSource.removeListener(mapDataListener) - } - - fun onNewScreenPosition() { - if (overlay == null) return - viewLifecycleScope.launch { updateCurrentScreenArea() } - } - - private suspend fun updateCurrentScreenArea() { - val zoom = map.cameraPosition.zoom - if (zoom < MIN_ZOOM) return - val displayedArea = withContext(Dispatchers.Main) { map.screenAreaToBoundingBox() } - val tilesRect = displayedArea.enclosingTilesRect(TILES_ZOOM) - // area too big -> skip (performance) - if (tilesRect.size > 32) return - val isNewRect = lastDisplayedRect?.contains(tilesRect) != true - if (!isNewRect) return - - lastDisplayedRect = tilesRect - // Check QuestPinsManager::updateCurrentScreenArea for an explanation what this updateJob - // stuff is about. - updateJob?.cancel() - updateJob = viewLifecycleScope.launch { - val bbox = tilesRect.asBoundingBox(TILES_ZOOM) - setStyledElements(bbox) - } - } - - private fun clear() { - lastDisplayedRect = null - viewLifecycleScope.launch { - mapDataInViewMutex.withLock { mapDataInView.clear() } - withContext(Dispatchers.Main) { mapComponent.clear() } - } - } - - private suspend fun setStyledElements(bbox: BoundingBox) { - val mapData = mapDataSourceMutex.withLock { - withContext(Dispatchers.IO) { mapDataSource.getMapDataWithGeometry(bbox) } - } - val styledElements = mapDataInViewMutex.withLock { - val overlay = overlay ?: return - mapDataInView.clear() - createStyledElementsByKey(overlay, mapData).forEach { (key, styledElement) -> - mapDataInView[key] = styledElement - } - mapDataInView.values.toList() - } - mapComponent.set(styledElements) - } - - private suspend fun updateStyledElements(updated: MapDataWithGeometry, deleted: Collection) { - val styledElements = mapDataInViewMutex.withLock { - val displayedBBox = lastDisplayedRect?.asBoundingBox(TILES_ZOOM) ?: return - var hasChanges = false - val overlay = overlay ?: return - - deleted.forEach { - if (mapDataInView.remove(it) != null) hasChanges = true - } - val styledElementsByKey = createStyledElementsByKey(overlay, updated).toMap() - // elements that used to be displayed in the overlay but now not anymore - updated.forEach { - if (!styledElementsByKey.containsKey(it.key)) { - if (mapDataInView.remove(it.key) != null) hasChanges = true - } - } - // elements that are either newly displayed or which were updated - styledElementsByKey.forEach { (key, styledElement) -> - if (displayedBBox.intersect(styledElement.geometry.bounds)) { - mapDataInView[key] = styledElement - hasChanges = true - } else { - if (mapDataInView.remove(key) != null) hasChanges = true - } - } - - if (!hasChanges) return - - mapDataInView.values.toList() - } - mapComponent.set(styledElements) - } - - private fun createStyledElementsByKey( - overlay: Overlay, - mapData: MapDataWithGeometry - ): Sequence> = - overlay.getStyledElements(mapData).mapNotNull { (element, style) -> - val key = element.key - val geometry = mapData.getGeometry(element.type, element.id) ?: return@mapNotNull null - key to StyledElement(element, geometry, style) - } - - companion object { - private const val TILES_ZOOM = 16 - private const val MIN_ZOOM = 14 - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/CurrentLocationMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/CurrentLocationMapComponent.kt deleted file mode 100644 index e6ba0e90c9a..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/CurrentLocationMapComponent.kt +++ /dev/null @@ -1,219 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.animation.TypeEvaluator -import android.animation.ValueAnimator -import android.content.Context -import android.view.animation.AccelerateDecelerateInterpolator -import androidx.annotation.UiThread -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import com.google.gson.JsonObject -import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.screens.main.map.maplibre.clear -import de.westnordost.streetcomplete.screens.main.map.maplibre.inMeters -import de.westnordost.streetcomplete.screens.main.map.maplibre.toPoint -import de.westnordost.streetcomplete.util.ktx.isApril1st -import de.westnordost.streetcomplete.util.math.normalizeDegrees -import de.westnordost.streetcomplete.util.math.normalizeLongitude -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.Style -import org.maplibre.android.style.expressions.Expression.* -import org.maplibre.android.style.layers.CircleLayer -import org.maplibre.android.style.layers.Layer -import org.maplibre.android.style.layers.Property -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.layers.SymbolLayer -import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.compose.location.LocationMeasurement -import org.maplibre.geojson.Feature -import org.maplibre.spatialk.geojson.Position -import org.maplibre.spatialk.units.International - -/** Takes care of showing the location + direction + accuracy marker on the map */ -class CurrentLocationMapComponent(context: Context, mapStyle: Style, private val map: MapLibreMap) : - DefaultLifecycleObserver { - - private val locationSource = GeoJsonSource(SOURCE) - private val locationAnimation = ValueAnimator() - - /** The location the GPS location dot on the map should be animated to */ - var targetLocation: LocationMeasurement? = null - @UiThread set(value) { - if (field == value) return - field = value - val location = this.location - if (location == null || value == null) { - locationAnimation.cancel() - this.location = value - update() - } else { - locationAnimation.setObjectValues(location, value) - locationAnimation.setEvaluator(locationTypeEvaluator) - locationAnimation.start() - } - } - - /** The location of the GPS location dot on the map (animated) */ - var location: LocationMeasurement? = null - private set - - private val locationTypeEvaluator = object : TypeEvaluator { - override fun evaluate(fraction: Float, s: LocationMeasurement, e: LocationMeasurement): LocationMeasurement { - val sp = s.position - val ep = e.position - val sa = s.horizontalAccuracy - val ea = e.horizontalAccuracy - // Preserve measuredAt from the target measurement while animating its position and accuracy. - return e.copy( - position = Position( - longitude = normalizeLongitude(sp.longitude + (ep.longitude - sp.longitude) * fraction), - latitude = sp.latitude + (ep.latitude - sp.latitude) * fraction, - altitude = ep.altitude, - ), - horizontalAccuracy = if (ea != null && sa != null) { - sa + (ea - sa) * fraction.toDouble() - } else { null } - ) - } - } - - /** The view rotation angle in degrees. Null if not set (yet) */ - var targetRotation: Float? = null - @UiThread set(value) { - if (field == value) return - field = value - val rotation = this.rotation - if (rotation == null || value == null) { - rotationAnimation.cancel() - this.rotation = value - update() - } else { - rotationAnimation.setFloatValues(rotation, normalizeDegrees(value, rotation - 180)) - rotationAnimation.start() - } - } - private val rotationAnimation = ValueAnimator() - - var rotation: Float? = null - private set - - val layers: List = listOfNotNull( - CircleLayer("accuracy", SOURCE) - .withProperties( - circleColor(context.resources.getColor(R.color.location_dot)), - circleOpacity(0.15f), - circleRadius(inMeters(get("radius"))), - circleStrokeColor(context.resources.getColor(R.color.location_dot)), - circleStrokeWidth(1.0f), - circleStrokeOpacity(0.5f), - circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP) - ), - SymbolLayer("direction", SOURCE) - .withFilter(has("rotation")) - .withProperties( - iconImage("directionImg"), - iconAllowOverlap(true), - iconIgnorePlacement(true), - iconRotate(get("rotation")), - iconPitchAlignment(Property.ICON_PITCH_ALIGNMENT_MAP) - ), - SymbolLayer("location-shadow", SOURCE) - .withProperties( - iconImage("shadowImg"), - iconAllowOverlap(true), - iconIgnorePlacement(true), - iconPitchAlignment(Property.ICON_PITCH_ALIGNMENT_MAP) - ), - CircleLayer("location", SOURCE) - .withProperties( - circleColor(context.resources.getColor(R.color.location_dot)), - circleRadius(8.0f), - circleStrokeWidth(2.0f), - circleStrokeColor("#fff"), - circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP) - ), - if (isApril1st()) { - SymbolLayer("location-nyan", SOURCE) - .withProperties( - iconImage("nyanImg"), - iconSize(2.0f), - iconAllowOverlap(true), - iconIgnorePlacement(true) - ) - } else { - null - } - ) - - init { - locationAnimation.duration = 600L - locationAnimation.interpolator = AccelerateDecelerateInterpolator() - locationAnimation.addUpdateListener { - location = locationAnimation.animatedValue as LocationMeasurement - update() - } - - rotationAnimation.duration = 200L - rotationAnimation.interpolator = AccelerateDecelerateInterpolator() - rotationAnimation.addUpdateListener { - rotation = rotationAnimation.animatedValue as Float - update() - } - - if (!isApril1st()) { - mapStyle.addImage("directionImg", context.getDrawable(R.drawable.location_view_direction)!!) - mapStyle.addImage("shadowImg", context.getDrawable(R.drawable.location_shadow)!!) - } else { - mapStyle.addImage("nyanImg", context.getDrawable(R.drawable.location_nyan)!!) - } - - locationSource.isVolatile = true - mapStyle.addSource(locationSource) - } - - override fun onPause(owner: LifecycleOwner) { - locationAnimation.pause() - rotationAnimation.pause() - } - - override fun onResume(owner: LifecycleOwner) { - locationAnimation.resume() - rotationAnimation.resume() - } - - override fun onDestroy(owner: LifecycleOwner) { - locationAnimation.cancel() - rotationAnimation.cancel() - } - - private fun hide() { - locationSource.clear() - } - - private fun show() { - update() - } - - /** Update the GPS position shown on the map */ - private fun update() { - val location = this.location - if (location == null) { - locationSource.clear() - return - } - val pos = location.position - - val p = JsonObject() - p.addProperty("radius", location.horizontalAccuracy?.toDouble(International.Meters)) - rotation?.let { p.addProperty("rotation", it) } - map.style?.getLayerAs("accuracy")?.setProperties( - circleRadius(inMeters(get("radius"), pos.latitude)) - ) - locationSource.setGeoJson(Feature.fromGeometry(LatLon(pos.latitude, pos.longitude).toPoint(), p)) - } - - companion object { - private const val SOURCE = "location-source" - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/DownloadedAreaMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/DownloadedAreaMapComponent.kt deleted file mode 100644 index 61858f0a2e8..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/DownloadedAreaMapComponent.kt +++ /dev/null @@ -1,56 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.content.Context -import androidx.core.graphics.drawable.toBitmap -import de.westnordost.streetcomplete.ApplicationConstants -import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.data.download.tiles.TilePos -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.data.osm.mapdata.toPolygon -import de.westnordost.streetcomplete.screens.main.map.maplibre.toPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.style.layers.FillLayer -import org.maplibre.android.style.layers.Layer -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.geojson.Polygon - -class DownloadedAreaMapComponent(private val context: Context, private val map: MapLibreMap) { - - private val downloadedAreaSource = GeoJsonSource("downloaded-area-source") - - val layers: List = listOf( - FillLayer("downloaded-area", "downloaded-area-source") - .withProperties( - fillPattern("downloaded_area_hatching"), - fillOpacity(0.6f) - ) - ) - - init { - map.style?.addImage( - "downloaded_area_hatching", - context.getDrawable(R.drawable.downloaded_area_hatching)!!.toBitmap() - ) - downloadedAreaSource.isVolatile = true - map.style?.addSource(downloadedAreaSource) - } - - suspend fun set(tiles: Collection) { - val zoom = ApplicationConstants.DOWNLOAD_TILE_ZOOM - val world = listOf( - LatLon(+90.0, -180.0), - LatLon(-90.0, -180.0), - LatLon(-90.0, +180.0), - LatLon(+90.0, +180.0), - LatLon(+90.0, -180.0), - ) - val holes = tiles.map { it.asBoundingBox(zoom).toPolygon().asReversed() } - val polygons = listOf(world) + holes - - val feature = Polygon.fromLngLats(polygons.map { polygon -> polygon.map { it.toPoint() } }) - withContext(Dispatchers.Main) { downloadedAreaSource.setGeoJson(feature) } - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/FocusGeometryMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/FocusGeometryMapComponent.kt deleted file mode 100644 index 47534eba24e..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/FocusGeometryMapComponent.kt +++ /dev/null @@ -1,168 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.animation.TimeAnimator -import android.content.ContentResolver -import android.provider.Settings -import androidx.annotation.UiThread -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry -import de.westnordost.streetcomplete.screens.main.map.maplibre.CameraPosition -import de.westnordost.streetcomplete.screens.main.map.maplibre.Padding -import de.westnordost.streetcomplete.screens.main.map.maplibre.camera -import de.westnordost.streetcomplete.screens.main.map.maplibre.clear -import de.westnordost.streetcomplete.screens.main.map.maplibre.getEnclosingCamera -import de.westnordost.streetcomplete.screens.main.map.maplibre.isArea -import de.westnordost.streetcomplete.screens.main.map.maplibre.isPoint -import de.westnordost.streetcomplete.screens.main.map.maplibre.toMapLibreGeometry -import de.westnordost.streetcomplete.screens.main.map.maplibre.updateCamera -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.style.layers.CircleLayer -import org.maplibre.android.style.layers.FillLayer -import org.maplibre.android.style.layers.Layer -import org.maplibre.android.style.layers.LineLayer -import org.maplibre.android.style.layers.Property -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.sources.GeoJsonSource -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt -import kotlin.math.sin - -/** Display element geometry and enables focussing on given geometry. I.e. to highlight the geometry - * of the element a selected quest refers to. Also zooms to the element in question so that it is - * contained in the screen area */ -class FocusGeometryMapComponent(private val contentResolver: ContentResolver, private val map: MapLibreMap) : - DefaultLifecycleObserver { - - private val focusedGeometrySource = GeoJsonSource(SOURCE) - - private var previousCameraPosition: CameraPosition? = null - - private val animation: TimeAnimator - private var animationTick: Int = 0 - - val layers: List = listOf( - FillLayer("focus-geo-fill", SOURCE) - .withFilter(isArea()) - .withProperties( - fillColor("#D14000"), - fillOpacity(0.3f) - ), - LineLayer("focus-geo-lines", SOURCE) - // both polygon and line - .withProperties( - lineWidth(10f), - lineColor("#D14000"), - lineOpacity(0.7f), - lineCap(Property.LINE_CAP_ROUND) - ), - CircleLayer("focus-geo-circle", SOURCE) - .withFilter(isPoint()) - .withProperties( - circleColor("#D14000"), - circleRadius(12f), - circleOpacity(0.7f) - ), - ) - - init { - focusedGeometrySource.isVolatile = true - map.style?.addSource(focusedGeometrySource) - animation = TimeAnimator() - animation.setTimeListener { _, _, _ -> - // we don't care about delta time etc because if this function is called rarely - // when device slow etc, the animation should just slow down rather than look - // jarring - animateGeometry() - } - } - - override fun onPause(owner: LifecycleOwner) { - animation.pause() - } - - override fun onResume(owner: LifecycleOwner) { - animation.resume() - } - - override fun onDestroy(owner: LifecycleOwner) { - animation.cancel() - } - - /** Show the given geometry. Previously shown geometry is replaced. */ - @UiThread fun showGeometry(geometry: ElementGeometry) { - focusedGeometrySource.setGeoJson(geometry.toMapLibreGeometry()) - val animatorDurationScale = Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) - if (animatorDurationScale > 0f) animation.start() - } - - private fun animateGeometry() { - // rather than editing the style, it is recommended to use feature-state for things like - // this. However, this is not implemented on Android yet. See - // https://github.com/maplibre/maplibre-native/issues/1698 - animationTick++ - val breathing = (sin(animationTick++ * 0.03f) / 2f + 0.5f) // 0.0 .. 1.0 - val widthFactor = breathing + 0.75f // 0.75 .. 1.5 - val opacity = (1f - breathing) * 0.5f + 0.15f // 0.525 .. 0.9 - map.style?.getLayerAs("focus-geo-lines")?.setProperties( - lineWidth(10f * widthFactor), - lineOpacity(opacity), - ) - map.style?.getLayerAs("focus-geo-circle")?.setProperties( - circleRadius(12f * widthFactor), - circleOpacity(opacity) - ) - } - - /** Hide all shown geometry */ - @UiThread fun clearGeometry() { - focusedGeometrySource.clear() - animation.end() - } - - @UiThread fun beginFocusGeometry(g: ElementGeometry, padding: Padding?) { - val targetPos = map.getEnclosingCamera(g, padding) ?: return - - val currentPos = map.camera - // limit max zoom to not zoom in to the max when zooming in on points; - // also zoom in a bit less to have a padding around the zoomed-in element - val targetZoom = min(targetPos.zoom - 0.75, 19.0) - - val zoomDiff = abs(currentPos.zoom - targetZoom) - val zoomTime = max(450, (zoomDiff * 450).roundToInt()) - - map.updateCamera(zoomTime, contentResolver) { - position = targetPos.position - this.padding = targetPos.padding - // also, only zoom if diff big enough - if (zoomDiff > 0.5) zoom = targetZoom - } - - if (previousCameraPosition == null) previousCameraPosition = currentPos - } - - @UiThread fun clearFocusGeometry() { - previousCameraPosition = null - } - - @UiThread fun endFocusGeometry() { - val pos = previousCameraPosition - if (pos != null) { - val currentPos = map.cameraPosition - val zoomTime = max(300, (abs(currentPos.zoom - pos.zoom) * 300).roundToInt()) - - map.updateCamera(zoomTime, contentResolver) { - position = pos.position - zoom = pos.zoom - padding = null - } - } - previousCameraPosition = null - } - - companion object { - private const val SOURCE = "focus-geometry-source" - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/GeometryMarkersMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/GeometryMarkersMapComponent.kt deleted file mode 100644 index a616d79de20..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/GeometryMarkersMapComponent.kt +++ /dev/null @@ -1,127 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.content.Context -import android.content.res.Resources -import androidx.annotation.UiThread -import com.google.gson.JsonObject -import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry -import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry -import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry -import de.westnordost.streetcomplete.resources.Res -import de.westnordost.streetcomplete.resources.preset_maki_circle -import de.westnordost.streetcomplete.screens.main.map.createIconBitmap -import de.westnordost.streetcomplete.screens.main.map.maplibre.MapImages -import de.westnordost.streetcomplete.screens.main.map.maplibre.clear -import de.westnordost.streetcomplete.screens.main.map.maplibre.isArea -import de.westnordost.streetcomplete.screens.main.map.maplibre.isPoint -import de.westnordost.streetcomplete.screens.main.map.maplibre.toMapLibreGeometry -import de.westnordost.streetcomplete.screens.main.map.maplibre.toPoint -import de.westnordost.streetcomplete.ui.common.quest.Marker -import de.westnordost.streetcomplete.view.toAndroidResourceId -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.style.expressions.Expression.* -import org.maplibre.android.style.layers.FillLayer -import org.maplibre.android.style.layers.Layer -import org.maplibre.android.style.layers.LineLayer -import org.maplibre.android.style.layers.Property -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.layers.SymbolLayer -import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.geojson.Feature -import org.maplibre.geojson.FeatureCollection - -/** Manages putting some generic geometry markers with an optional drawable on the map. I.e. to - * show the geometry of elements surrounding the selected quest */ -class GeometryMarkersMapComponent( - private val context: Context, - private val map: MapLibreMap, - private val mapImages: MapImages -) { - - private val geometrySource = GeoJsonSource(SOURCE) - - val layers: List = listOf( - FillLayer("geo-fill", SOURCE) - .withFilter(isArea()) - .withProperties( - fillColor("#D140D0"), - fillOpacity(0.3f) - ), - LineLayer("geo-lines", SOURCE) - // both polygon and line - .withProperties( - lineWidth(10f), - lineColor("#D140D0"), - lineOpacity(0.5f), - lineCap(Property.LINE_CAP_ROUND) - ), - SymbolLayer("geo-symbols", SOURCE) - .withFilter(isPoint()) - .withProperties( - iconColor("#D140D0"), - iconImage(get("icon")), - iconSize(interpolate(linear(), zoom(), stop(17, 0.5f), stop(19, 1f))), - iconAllowOverlap(true), - textField(get("label")), - textAnchor(Property.TEXT_ANCHOR_TOP), - textOffset(arrayOf(0f, 1f)), - textSize(16 * context.resources.configuration.fontScale), - textColor("#D140D0"), - textFont(arrayOf("Roboto Bold")), - textOptional(true) - ) - ) - - init { - geometrySource.isVolatile = true - map.style?.addSource(geometrySource) - } - - suspend fun setAll(markers: Iterable) { - val icons = markers.map { it.icon ?: Res.drawable.preset_maki_circle }.mapNotNull { it.toAndroidResourceId() } - mapImages.addOnce(icons) { - val name = context.resources.getResourceEntryName(it) - val sdf = name.startsWith("preset_") - createIconBitmap(context, it, sdf) to sdf - } - val features = markers.flatMap { it.toFeatures(context.resources) } - - withContext(Dispatchers.Main) { - geometrySource.clear() - geometrySource.setGeoJson(FeatureCollection.fromFeatures(features)) - } - } - - @UiThread fun clear() { - geometrySource.clear() - } - - private fun update() { - } - - companion object { - private const val SOURCE = "geometry-source" - } -} - -private fun Marker.toFeatures(resources: Resources): List { - val features = ArrayList(3) - // point marker or any marker with title or icon - if (icon != null || title != null || geometry is ElementPointGeometry) { - val p = JsonObject() - val mustHaveIcon = (icon ?: Res.drawable.preset_maki_circle).toAndroidResourceId()!! - p.addProperty("icon", resources.getResourceEntryName(mustHaveIcon)) - if (title != null) { - p.addProperty("label", title) - } - features.add(Feature.fromGeometry(geometry.center.toPoint(), p)) - } - - // polygon / polylines marker(s) - if (geometry is ElementPolygonsGeometry || geometry is ElementPolylinesGeometry) { - features.add(Feature.fromGeometry(geometry.toMapLibreGeometry())) - } - return features -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/PinsMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/PinsMapComponent.kt deleted file mode 100644 index af1e4d5060f..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/PinsMapComponent.kt +++ /dev/null @@ -1,199 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.content.ContentResolver -import android.content.Context -import androidx.annotation.UiThread -import com.google.gson.JsonObject -import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.screens.main.map.createPinBitmap -import de.westnordost.streetcomplete.screens.main.map.maplibre.MapImages -import de.westnordost.streetcomplete.screens.main.map.maplibre.Padding -import de.westnordost.streetcomplete.screens.main.map.maplibre.clear -import de.westnordost.streetcomplete.screens.main.map.maplibre.getEnclosingCamera -import de.westnordost.streetcomplete.screens.main.map.maplibre.toLatLon -import de.westnordost.streetcomplete.screens.main.map.maplibre.toPoint -import de.westnordost.streetcomplete.screens.main.map.maplibre.updateCamera -import de.westnordost.streetcomplete.util.math.enclosingBoundingBox -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.maplibre.android.geometry.LatLng -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.style.expressions.Expression.all -import org.maplibre.android.style.expressions.Expression.any -import org.maplibre.android.style.expressions.Expression.division -import org.maplibre.android.style.expressions.Expression.get -import org.maplibre.android.style.expressions.Expression.gt -import org.maplibre.android.style.expressions.Expression.gte -import org.maplibre.android.style.expressions.Expression.literal -import org.maplibre.android.style.expressions.Expression.log2 -import org.maplibre.android.style.expressions.Expression.lte -import org.maplibre.android.style.expressions.Expression.sum -import org.maplibre.android.style.expressions.Expression.toNumber -import org.maplibre.android.style.expressions.Expression.zoom -import org.maplibre.android.style.layers.CircleLayer -import org.maplibre.android.style.layers.Layer -import org.maplibre.android.style.layers.Property -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.layers.SymbolLayer -import org.maplibre.android.style.sources.GeoJsonOptions -import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.geojson.Feature -import org.maplibre.geojson.FeatureCollection -import org.maplibre.geojson.Point -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt - -/** Takes care of displaying pins on the map, e.g. quest pins or pins for recent edits */ -class PinsMapComponent( - private val context: Context, - private val contentResolver: ContentResolver, - private val map: MapLibreMap, - private val mapImages: MapImages, - private val onClickPin: (properties: Map) -> Unit -) { - private val pinsSource = GeoJsonSource(SOURCE, - GeoJsonOptions() - .withCluster(true) - .withClusterMaxZoom(CLUSTER_MAX_ZOOM) - .withClusterRadius(55) - ) - - val layers: List = listOf( - SymbolLayer("pin-cluster-layer", SOURCE) - .withFilter(all( - gte(zoom(), 13f), - lte(zoom(), CLUSTER_MAX_ZOOM), - gt(toNumber(get("point_count")), 1) - )) - .withProperties( - iconImage("cluster-circle"), - iconSize(sum(literal(0.5f), division(log2(get("point_count")), literal(10f)))), - textField(get("point_count")), - textFont(arrayOf("Roboto Regular")), - textOffset(arrayOf(0f, 0.1f)), - textSize(sum(literal(15f), division(log2(get("point_count")), literal(1.5f)))), - iconAllowOverlap(true), - textAllowOverlap(true), - iconIgnorePlacement(true), - textIgnorePlacement(true), - ), - CircleLayer("pin-dot-layer", SOURCE) - .withFilter(any( - gt(zoom(), CLUSTER_MAX_ZOOM), - all(gte(zoom(), 14f), lte(toNumber(get("point_count")), 1)) - )) - .withProperties( - circleColor("white"), - circleStrokeColor("#aaaaaa"), - circleRadius(5f), - circleStrokeWidth(1f), - circleTranslate(arrayOf(0f, -8f)), // so that it hides behind the pin - circleTranslateAnchor(Property.CIRCLE_TRANSLATE_ANCHOR_VIEWPORT), - ), - SymbolLayer("pins-layer", SOURCE) - .withFilter(gt(zoom(), CLUSTER_MAX_ZOOM)) - .withProperties( - iconImage(get("icon-image")), - // constant icon size because click area would become a bit too small and more - // importantly, dynamic size per zoom + collision doesn't work together well, it - // results in a lot of flickering. - iconSize(1f), - - iconPadding(arrayOf(-2.5f, 0f, -7f, 2.5f)), - iconOffset(arrayOf(-4.5f, -34.5f)), - iconAllowOverlap(false), - iconIgnorePlacement(false), - symbolSortKey(get("icon-order")), - ) - ) - - /** Shows/hides the pins */ - @UiThread fun setVisible(value: Boolean) { - val visibility = if (value) Property.VISIBLE else Property.NONE - layers.forEach { it.setProperties(visibility(visibility)) } - } - - init { - pinsSource.isVolatile = true - map.style?.addImageAsync("cluster-circle", context.getDrawable(R.drawable.pin_circle)!!) - map.style?.addSource(pinsSource) - map.addOnMapClickListener(::onClick) - } - - /** Show given pins. Previously shown pins are replaced with these. */ - suspend fun set(pins: Collection) { - val icons = pins.map { it.icon } - mapImages.addOnce(icons) { createPinBitmap(context, it) to false } - val features = pins.map { it.toFeature() } - val mapLibreFeatures = FeatureCollection.fromFeatures(features) - withContext(Dispatchers.Main) { pinsSource.setGeoJson(mapLibreFeatures) } - } - - /** Clear pins */ - @UiThread fun clear() { - pinsSource.clear() - } - - private fun onClick(position: LatLng): Boolean { - val feature = map.queryRenderedFeatures( - map.projection.toScreenLocation(position), - *arrayOf("pins-layer", "pin-cluster-layer") - ).firstOrNull() ?: return false - - val properties = feature.properties() - - if (properties?.has("point_count") == true) { - zoomToCluster(feature) - } else { - onClickPin(properties?.toMap().orEmpty()) - } - return true - } - - private fun zoomToCluster(feature: Feature) { - val leaves = pinsSource.getClusterLeaves(feature, Long.MAX_VALUE, 0L) - val bbox = leaves.features() - ?.mapNotNull { (it.geometry() as? Point)?.toLatLon() } - ?.enclosingBoundingBox() - ?: return - val targetPos = map.getEnclosingCamera(bbox, Padding(0.0, 0.0, 0.0, 0.0)) ?: return - - // don't zoom in fully: leave some space to show the full pins, and limit max zoom - val targetZoom = min(targetPos.zoom - 0.25, 19.0) - - val zoomDiff = abs(map.cameraPosition.zoom - targetZoom) - val zoomTime = max(450, (zoomDiff * 450).roundToInt()) - - map.updateCamera(zoomTime, contentResolver) { - position = targetPos.position - padding = targetPos.padding - this.zoom = targetZoom - } - } - - private fun Pin.toFeature(): Feature { - val p = JsonObject() - p.addProperty("icon-image", context.resources.getResourceEntryName(icon)) - p.addProperty("icon-order", order + 50) - properties.forEach { p.addProperty(it.first, it.second) } - return Feature.fromGeometry(position.toPoint(), p) - } - - companion object { - private const val SOURCE = "pins-source" - private const val CLUSTER_MAX_ZOOM = 14 - } -} - -data class Pin( - val position: LatLon, - val icon: Int, - val properties: Collection> = emptyList(), - val order: Int = 0 -) - -private fun JsonObject.toMap(): Map = - entrySet().associate { it.key to it.value.asString } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/SceneMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/SceneMapComponent.kt deleted file mode 100644 index e31c57262f8..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/SceneMapComponent.kt +++ /dev/null @@ -1,97 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.content.Context -import android.content.res.Configuration -import android.provider.Settings -import androidx.annotation.UiThread -import de.westnordost.streetcomplete.screens.main.map.maplibre.awaitSetStyle -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.Style -import org.maplibre.android.style.expressions.Expression -import org.maplibre.android.style.expressions.Expression.* -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.layers.SymbolLayer -import org.maplibre.android.style.layers.TransitionOptions -import java.util.Locale - -/** Takes care of loading the base map with the right parameters (localization, night mode, style - * updates etc... */ -class SceneMapComponent( - private val context: Context, - private val map: MapLibreMap, -) { - /** Load the scene */ - suspend fun loadStyle(): Style { - val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - val isNightMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES - val mapFile = - if (isNightMode) "map_theme/streetcomplete-night.json" - else "map_theme/streetcomplete.json" - - val styleJsonString = context.resources.assets.open(mapFile) - .bufferedReader() - .use { it.readText() } - - val styleBuilder = Style.Builder().fromJson(styleJsonString) - val style = map.awaitSetStyle(styleBuilder) - withContext(Dispatchers.Main) { updateStyle() } - return style - } - - /** Updates part of the style depending on the user settings: - * Language, animator duration scale, font scale */ - @UiThread fun updateStyle() { - val style = map.style ?: return - - // apply global animator duration scale - val animatorDurationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) - style.transition = TransitionOptions( - (300 * animatorDurationScale).toLong(), - 0, - true - ) - - // apply localization - val nameLayers = listOf( - "labels-country", - "labels-localities", - "labels-road", - "labels-rivers", - "labels-streams", - ) - - val language = Locale.getDefault().language - nameLayers.forEach { layer -> - style.getLayerAs(layer)?.setProperties( - textFont(arrayOf("Roboto Regular")), - textField(localizedName(language)) - ) - } - - // apply Android font scaling - val textLayers = nameLayers + listOf( - "labels-housenumbers" - ) - - textLayers.forEach { layer -> - style.getLayerAs(layer)?.setProperties( - textSize(16 * context.resources.configuration.fontScale) - ) - } - } -} - -private fun localizedName(language: String): Expression { - // as defined in https://www.jawg.io/docs/apidocs/maps/streets-v2/ - val localizedName = listOf("name_$language", "name_ltn") - val getLocalizedName = coalesce(*localizedName.map { get(it) }.toTypedArray()) - return switchCase( - // localized name set and different as main name: show both - all(toBool(getLocalizedName), neq(get("name"), getLocalizedName)), - concat(get("name"), literal("\n"), getLocalizedName), - // otherwise just show the name - get("name"), - ) -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/SelectedPinsMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/SelectedPinsMapComponent.kt deleted file mode 100644 index 54f8433b2d5..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/SelectedPinsMapComponent.kt +++ /dev/null @@ -1,92 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.animation.ValueAnimator -import android.content.Context -import android.view.animation.OvershootInterpolator -import androidx.annotation.DrawableRes -import androidx.annotation.UiThread -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import com.google.gson.JsonObject -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.screens.main.map.createPinBitmap -import de.westnordost.streetcomplete.screens.main.map.maplibre.MapImages -import de.westnordost.streetcomplete.screens.main.map.maplibre.clear -import de.westnordost.streetcomplete.screens.main.map.maplibre.toPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.style.expressions.Expression.* -import org.maplibre.android.style.layers.Layer -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.layers.SymbolLayer -import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.geojson.Feature -import org.maplibre.geojson.FeatureCollection - -/** Takes care of displaying "selected" pins. Those pins are always shown on top of pins displayed - * by the [PinsMapComponent] */ -class SelectedPinsMapComponent( - private val context: Context, - private val map: MapLibreMap, - private val mapImages: MapImages -) : DefaultLifecycleObserver { - - private val selectedPinsSource = GeoJsonSource("selected-pins-source") - private val animation: ValueAnimator - - val layers: List = listOf( - SymbolLayer("selected-pins-layer", "selected-pins-source") - .withProperties( - iconImage(get("icon-image")), - iconSize(0.5f), - iconOffset(listOf(-4.5f, -34.5f).toTypedArray()), - ) - ) - - init { - selectedPinsSource.isVolatile = true - map.style?.addSource(selectedPinsSource) - animation = ValueAnimator.ofFloat(0.5f, 1.5f) - animation.duration = 300 - animation.interpolator = OvershootInterpolator() - animation.addUpdateListener { animatePin(it.animatedValue as Float) } - } - - override fun onPause(owner: LifecycleOwner) { - animation.pause() - } - - override fun onResume(owner: LifecycleOwner) { - animation.resume() - } - - override fun onDestroy(owner: LifecycleOwner) { - animation.cancel() - } - - /** Show selected pins with the given icon at the given positions. "Selected pins" are not - * related to pins, they are just visuals that are displayed on top of the normal pins and look - * highlighted/selected. */ - suspend fun set(@DrawableRes iconResId: Int, pinPositions: Collection) { - mapImages.addOnce(iconResId) { createPinBitmap(context, it) to false } - val p = JsonObject() - p.addProperty("icon-image", context.resources.getResourceEntryName(iconResId)) - val points = pinPositions.map { Feature.fromGeometry(it.toPoint(), p) } - withContext(Dispatchers.Main) { - selectedPinsSource.setGeoJson(FeatureCollection.fromFeatures(points)) - animation.start() - } - } - - private fun animatePin(value: Float) { - map.style?.getLayerAs("selected-pins-layer")?.setProperties( - iconSize(value), - ) - } - - /** Clear the display of any selected pins */ - @UiThread fun clear() { - selectedPinsSource.clear() - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/StyleableOverlayMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/StyleableOverlayMapComponent.kt deleted file mode 100644 index a3f468f73ba..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/StyleableOverlayMapComponent.kt +++ /dev/null @@ -1,416 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.content.Context -import android.content.res.Configuration -import androidx.annotation.UiThread -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import com.google.gson.JsonObject -import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.Element -import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey -import de.westnordost.streetcomplete.data.osm.mapdata.ElementType -import de.westnordost.streetcomplete.data.osm.mapdata.key -import de.westnordost.streetcomplete.data.overlays.OverlayColor.Invisible -import de.westnordost.streetcomplete.data.overlays.OverlayStyle -import de.westnordost.streetcomplete.screens.main.map.createIconBitmap -import de.westnordost.streetcomplete.screens.main.map.maplibre.MapImages -import de.westnordost.streetcomplete.screens.main.map.maplibre.clear -import de.westnordost.streetcomplete.screens.main.map.maplibre.inMeters -import de.westnordost.streetcomplete.screens.main.map.maplibre.isArea -import de.westnordost.streetcomplete.screens.main.map.maplibre.isLine -import de.westnordost.streetcomplete.screens.main.map.maplibre.isPoint -import de.westnordost.streetcomplete.screens.main.map.maplibre.queryRenderedFeatures -import de.westnordost.streetcomplete.screens.main.map.maplibre.toMapLibreGeometry -import de.westnordost.streetcomplete.screens.main.map.maplibre.toPoint -import de.westnordost.streetcomplete.view.toAndroidResourceId -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.maplibre.android.geometry.LatLng -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.style.expressions.Expression.* -import org.maplibre.android.style.layers.FillExtrusionLayer -import org.maplibre.android.style.layers.FillLayer -import org.maplibre.android.style.layers.Layer -import org.maplibre.android.style.layers.LineLayer -import org.maplibre.android.style.layers.Property -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.layers.SymbolLayer -import org.maplibre.android.style.sources.GeoJsonOptions -import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.geojson.Feature -import org.maplibre.geojson.FeatureCollection - -/** Takes care of displaying styled map data */ -class StyleableOverlayMapComponent( - private val context: Context, - private val map: MapLibreMap, - private val mapImages: MapImages, - private val clickRadius: Float, - private val onClickElement: (key: ElementKey) -> Unit -) { - - private val overlaySource = GeoJsonSource( - SOURCE, - GeoJsonOptions().withMinZoom(MIN_ZOOM) - ) - - private val isNightMode: Boolean get() { - val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - return currentNightMode == Configuration.UI_MODE_NIGHT_YES - } - - private val sideLinesProperties = arrayOf( - lineCap(Property.LINE_CAP_BUTT), - lineJoin(Property.LINE_JOIN_ROUND), - lineColor(get("color")), - lineOpacity(get("opacity")), - lineOffset(inMeters(get("offset"))), - lineWidth(inMeters(get("width"))), - ) - - private val sideLineFilters = arrayOf( - isLine(), - gte(zoom(), MIN_ZOOM), - has("offset"), - ) - - val sideLayers: List = listOf( - LineLayer("overlay-lines-side", SOURCE) - .withFilter(all(*sideLineFilters, not(has("bridge")), not(has("dashed")))) - .withProperties(*sideLinesProperties), - LineLayer("overlay-lines-dashed-side", SOURCE) - .withFilter(all(*sideLineFilters, not(has("bridge")), has("dashed"))) - .withProperties(*sideLinesProperties, lineDasharray(arrayOf(1.5f, 1f))), - ) - - val sideLayersBridge: List = listOf( - LineLayer("overlay-lines-bridge-side", SOURCE) - .withFilter(all(*sideLineFilters, has("bridge"), not(has("dashed")))) - .withProperties(*sideLinesProperties), - LineLayer("overlay-lines-dashed-bridge-side", SOURCE) - .withFilter(all(*sideLineFilters, has("bridge"), has("dashed"))) - .withProperties(*sideLinesProperties, lineDasharray(arrayOf(1.5f, 1f))), - ) - - val layers: List = listOf( - LineLayer("overlay-lines-casing", SOURCE) - .withFilter(all( - isLine(), - gte(zoom(), MIN_ZOOM), - not(has("offset")), - not(has("dashed")) - )) - .withProperties( - lineCap(Property.LINE_CAP_ROUND), - lineJoin(Property.LINE_JOIN_ROUND), - lineColor(get("outline-color")), - lineOpacity(get("opacity")), - lineGapWidth(inMeters(get("width"))), - lineWidth(inMeters(0.5f)), - ), - FillLayer("overlay-fills", SOURCE) - .withFilter(all( - isArea(), - gte(zoom(), MIN_ZOOM) - )) - .withProperties( - fillColor(get("color")), - fillOpacity(get("opacity")), - ), - LineLayer("overlay-lines", SOURCE) - .withFilter(all( - isLine(), - gte(zoom(), MIN_ZOOM), - not(has("offset")), - not(has("dashed")) - )) - .withProperties( - lineCap(Property.LINE_CAP_ROUND), - lineJoin(Property.LINE_JOIN_ROUND), - lineColor(get("color")), - lineOpacity(get("opacity")), - lineWidth(inMeters(get("width"))), - ), - LineLayer("overlay-lines-dashed", SOURCE) - .withFilter(all( - isLine(), - gte(zoom(), MIN_ZOOM), - not(has("offset")), - has("dashed") - )) - .withProperties( - lineCap(Property.LINE_CAP_BUTT), // because of dashed - lineJoin(Property.LINE_JOIN_ROUND), - lineColor(get("color")), - lineOpacity(get("opacity")), - lineWidth(inMeters(get("width"))), - lineDasharray(arrayOf(1.5f, 1f)), - ), - LineLayer("overlay-fills-outline", SOURCE) - .withFilter(all( - isArea(), - gte(zoom(), MIN_ZOOM) - )) - .withProperties( - lineCap(Property.LINE_CAP_BUTT), - lineColor(get("outline-color")), - lineOpacity(get("opacity")), - lineWidth(inMeters(0.5f)), - ), - FillExtrusionLayer("overlay-heights", SOURCE) - .withFilter(all( - isArea(), - has("height"), - gte(zoom(), MIN_ZOOM) - )) - .withProperties( - fillExtrusionColor(get("color")), - fillExtrusionOpacity(1f), // can't use get("opacity"), data expressions not supported - fillExtrusionHeight(get("height")), - fillExtrusionBase(get("min-height")), - ), - ) - - val labelLayers = listOf( - SymbolLayer("overlay-symbols", SOURCE) - .withFilter(all( - gte(zoom(), 17f), - isPoint() - )) - .withProperties( - iconImage(get("icon")), - iconSize(interpolate(linear(), zoom(), stop(17, 0.5f), stop(19, 1f))), - textField(get("label")), - textFont(arrayOf("Roboto Regular")), - textAnchor(Property.TEXT_ANCHOR_TOP), - textOffset(switchCase(has("icon"), literal(arrayOf(0f, 1f)), literal(arrayOf(0f, 0f)))), - textSize(16 * context.resources.configuration.fontScale), - textColor(if (isNightMode) "#ccf" else "#124"), - textHaloColor(if (isNightMode) "#2e2e48" else "#fff"), - textHaloWidth(2.5f), - iconColor(if (isNightMode) "#ccf" else "#124"), - iconHaloColor(if (isNightMode) "#2e2e48" else "#fff"), - iconHaloWidth(2.5f), - textOptional(true), - iconAllowOverlap(true), - textAllowOverlap(step(zoom(), literal(false), stop(21, true))), - symbolZOrder(Property.SYMBOL_Z_ORDER_SOURCE), - ), - ) - - private val allLayers = layers + sideLayers + sideLayersBridge + labelLayers - - /** Shows/hides the map data */ - @UiThread fun setVisible(value: Boolean) { - val visibility = if (value) Property.VISIBLE else Property.NONE - allLayers.forEach { it.setProperties(visibility(visibility)) } - } - - init { - overlaySource.isVolatile = true - map.style?.addSource(overlaySource) - map.addOnMapClickListener(::onClick) - } - - /** Show given map data with each the given style */ - suspend fun set(styledElementWithGeometries: Collection) { - val icons = styledElementWithGeometries.mapNotNull { it.overlayStyle.getIcon() } - mapImages.addOnce(icons) { - val name = context.resources.getResourceEntryName(it) - val sdf = name.startsWith("preset_") - createIconBitmap(context, it, sdf) to sdf - } - val features = styledElementWithGeometries.flatMap { it.toFeatures() } - val mapLibreFeatures = FeatureCollection.fromFeatures(features) - withContext(Dispatchers.Main) { overlaySource.setGeoJson(mapLibreFeatures) } - } - - private fun onClick(position: LatLng): Boolean { - val feature = map.queryRenderedFeatures( - coordinates = map.projection.toScreenLocation(position), - radius = clickRadius, - layerIds = arrayOf("overlay-symbols", "overlay-lines", "overlay-lines-dashed", "overlay-fills") - ).firstOrNull() ?: return false - - val properties = feature.properties() ?: return false - - val elementKey = getElementKey(properties) - if (elementKey != null && !isDisabled(properties)) { - onClickElement(elementKey) - return true - } - return false - } - - private fun StyledElement.toFeatures(): List { - val p = getElementProperties(element.key, overlayStyle.disabled) - - return when (overlayStyle) { - is OverlayStyle.Point -> { - if (overlayStyle.icon != null) { - p.addProperty("icon", context.resources.getResourceEntryName(overlayStyle.icon.toAndroidResourceId()!!)) - } - if (overlayStyle.label != null) p.addProperty("label", overlayStyle.label) - - listOf(Feature.fromGeometry(geometry.center.toPoint(), p)) - } - is OverlayStyle.Polygon -> { - if (overlayStyle.color != Invisible) { - p.addProperty("color", overlayStyle.color.toRgbaString()) - p.addProperty("outline-color", overlayStyle.color.darkened().toRgbaString()) - p.addProperty("opacity", 0.8f) - } else { - p.addProperty("opacity", 0f) - } - - if (overlayStyle.height != null && overlayStyle.color != Invisible) { - p.addProperty("height", overlayStyle.height) - if (overlayStyle.minHeight != null) { - p.addProperty("min-height", overlayStyle.minHeight.coerceAtMost(overlayStyle.height)) - } - } - - val f = Feature.fromGeometry(geometry.toMapLibreGeometry(), p) - val point = if (overlayStyle.label != null || overlayStyle.icon != null) { - val pp = getElementProperties(element.key, overlayStyle.disabled) - val iconId = overlayStyle.icon?.toAndroidResourceId() - if (iconId != null) { - pp.addProperty("icon", context.resources.getResourceEntryName(iconId)) - } - if (overlayStyle.label != null) pp.addProperty("label", overlayStyle.label) - Feature.fromGeometry(geometry.center.toPoint(), pp) - } else { - null - } - - listOfNotNull(f, point) - } - is OverlayStyle.Polyline -> { - val line = geometry.toMapLibreGeometry() - val width = getLineWidth(element.tags) - if (isBridge(element.tags)) p.addProperty("bridge", true) - - val left = overlayStyle.strokeLeft?.let { - val p2 = p.deepCopy() - p2.addProperty("width", 3f) - p2.addProperty("offset", -(width / 2f + 1.5f)) - if (it.color != Invisible) { - p2.addProperty("color", it.color.toRgbaString()) - } else { - p2.addProperty("opacity", 0f) - } - if (it.dashed) p2.addProperty("dashed", true) - Feature.fromGeometry(line, p2) - } - - val right = overlayStyle.strokeRight?.let { - val p2 = p.deepCopy() - p2.addProperty("width", 3f) - p2.addProperty("offset", +(width / 2f + 1.5f)) - if (it.color != Invisible) { - p2.addProperty("color", it.color.toRgbaString()) - } else { - p2.addProperty("opacity", 0f) - } - if (it.dashed) p2.addProperty("dashed", true) - Feature.fromGeometry(line, p2) - } - - val center = overlayStyle.stroke.let { - val p2 = p.deepCopy() - p2.addProperty("width", width) - if (it != null && it.color != Invisible) { - p2.addProperty("color", it.color.toRgbaString()) - p2.addProperty("outline-color", it.color.darkened().toRgbaString()) - } else { - p2.addProperty("opacity", 0f) - } - if (it?.dashed == true) p2.addProperty("dashed", true) - Feature.fromGeometry(line, p2) - } - - val label = if (overlayStyle.label != null) { - Feature.fromGeometry( - geometry.center.toPoint(), - JsonObject().apply { addProperty("label", overlayStyle.label) } - ) - } else { - null - } - - listOfNotNull(left, right, center, label) - } - } - } - - private fun isDisabled(properties: JsonObject): Boolean = - properties[DISABLED]?.asBoolean ?: false - - private fun getElementKey(properties: JsonObject): ElementKey? { - val id = properties[ELEMENT_ID]?.asLong ?: return null - val type = properties[ELEMENT_TYPE]?.asString ?: return null - return ElementKey(ElementType.valueOf(type), id) - } - - private fun getElementProperties(key: ElementKey, disabled: Boolean): JsonObject { - val p = JsonObject() - p.addProperty(ELEMENT_ID, key.id) - p.addProperty(ELEMENT_TYPE, key.type.name) - if (disabled) p.addProperty(DISABLED, disabled) - return p - } - - /** Clear map data */ - @UiThread fun clear() { - overlaySource.clear() - } - - companion object { - private const val SOURCE = "overlay-source" - - private const val ELEMENT_TYPE = "element_type" - private const val ELEMENT_ID = "element_id" - private const val DISABLED = "clickable" - - private const val MIN_ZOOM = 14 - } -} - -data class StyledElement( - val element: Element, - val geometry: ElementGeometry, - val overlayStyle: OverlayStyle -) - -private fun Color.darkened(): Color = Color( - red = red * 0.67f, - green = green * 0.67f, - blue = blue * 0.67f, - alpha = alpha -) - -private fun Color.toRgbaString(): String { - val c = toArgb() - return "rgba(${(c shr 16) and 0xFF}, ${(c shr 8) and 0xFF}, ${c and 0xFF}, $alpha)" -} - -/** mimics width of line as seen in StreetComplete map style */ -private fun getLineWidth(tags: Map): Float = when (tags["highway"]) { - "motorway" -> 8f - "motorway_link" -> 4f - "trunk", "primary", "secondary", "tertiary" -> 6f - "service", "track", "busway" -> 3f - "path", "cycleway", "footway", "bridleway", "steps" -> 1.0f - null -> 2f - else -> 4f -} - -private fun isBridge(tags: Map): Boolean = - tags["bridge"] != null && tags["bridge"] != "no" - -private fun OverlayStyle.getIcon(): Int? = when (this) { - is OverlayStyle.Point -> icon?.toAndroidResourceId() - is OverlayStyle.Polygon -> icon?.toAndroidResourceId() - is OverlayStyle.Polyline -> null -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/TracksMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/TracksMapComponent.kt deleted file mode 100644 index 78f5c70d603..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/TracksMapComponent.kt +++ /dev/null @@ -1,177 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.components - -import android.animation.ValueAnimator -import android.content.Context -import android.view.animation.AccelerateDecelerateInterpolator -import androidx.annotation.UiThread -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import com.google.gson.JsonObject -import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.screens.main.map.maplibre.clear -import de.westnordost.streetcomplete.util.ktx.isApril1st -import de.westnordost.streetcomplete.util.math.normalizeLongitude -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.Style -import org.maplibre.android.style.expressions.Expression.* -import org.maplibre.android.style.layers.Layer -import org.maplibre.android.style.layers.LineLayer -import org.maplibre.android.style.layers.Property -import org.maplibre.android.style.layers.PropertyFactory.* -import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.geojson.Feature -import org.maplibre.geojson.FeatureCollection -import org.maplibre.geojson.LineString -import org.maplibre.geojson.Point -import kotlin.math.max - -/** Takes care of showing the path(s) walked on the map */ -class TracksMapComponent(context: Context, mapStyle: Style, private val map: MapLibreMap) : - DefaultLifecycleObserver { - /* There are two sources simply as a performance optimization: If there are thousands of - trackpoints, we don't want to update (=copy) the thousands of points each time a new - trackpoint is added. Instead, we only update a list of 100 trackpoints each time a new - trackpoint is added and every 100th time, we update the other layer. - - So, the list of points updated ~per second doesn't grow too long. - The animation source is just the last line, whose progress is animated. - */ - private val trackAnimationSource = GeoJsonSource("animate-track-source") - private val animation = ValueAnimator.ofFloat(0f, 1f) - - private val trackSource = GeoJsonSource("track-source") - private val oldTrackSource = GeoJsonSource("old-track-source") - - private data class Track(val trackpoints: MutableList, val isRecording: Boolean) - private var track: Track = Track(ArrayList(), false) - private var oldTracks: MutableList> = arrayListOf() - - private val commonTrackProperties get() = if (!isApril1st()) arrayOf( - lineWidth(6f), - lineColor(switchCase( - get("recording"), literal("#fe1616"), - literal("#536dfe") - )) - ) else arrayOf( - lineWidth(26f), - linePattern(switchCase( - get("recording"), literal("trackRecordImg"), - literal("trackImg") - )) - ) - - val layers: List = listOf( - LineLayer("animate-track", "animate-track-source") - .withProperties(*commonTrackProperties, lineOpacity(0.6f), lineCap(Property.LINE_CAP_ROUND), lineDasharray(arrayOf(0f, 2f))), - LineLayer("track", "track-source") - .withProperties(*commonTrackProperties, lineOpacity(0.6f), lineCap(Property.LINE_CAP_ROUND), lineDasharray(arrayOf(0f, 2f))), - LineLayer("old-track", "old-track-source") - .withProperties(*commonTrackProperties, lineOpacity(0.2f), lineCap(Property.LINE_CAP_ROUND), lineDasharray(arrayOf(0f, 2f))) - ) - - init { - trackAnimationSource.isVolatile = true - trackSource.isVolatile = true - oldTrackSource.isVolatile = true - - animation.duration = 600L - animation.interpolator = AccelerateDecelerateInterpolator() - animation.addUpdateListener { updateAnimatedTrack(it.animatedValue as Float) } - - if (isApril1st()) { - mapStyle.addImage("trackImg", context.getDrawable(R.drawable.track_nyan)!!) - mapStyle.addImage("trackRecordImg", context.getDrawable(R.drawable.track_nyan_record)!!) - } - - map.style?.addSource(trackAnimationSource) - map.style?.addSource(trackSource) - map.style?.addSource(oldTrackSource) - } - - override fun onPause(owner: LifecycleOwner) { - animation.pause() - } - - override fun onResume(owner: LifecycleOwner) { - animation.resume() - } - - override fun onDestroy(owner: LifecycleOwner) { - animation.cancel() - } - - /** Add a point to the current track */ - @UiThread fun addToCurrentTrack(pos: LatLon) { - track.trackpoints.add(pos) - // every 100th trackpoint, move older trackpoints to old tracks - if (track.trackpoints.size > 100) { - oldTracks.add(track.trackpoints.subList(0, 50).toMutableList()) - val lastPoints = track.trackpoints.subList(50, track.trackpoints.size).toMutableList() - track.trackpoints.clear() - track.trackpoints.addAll(lastPoints) - updateOldTracks() - } - updateTrack() - } - - /** Start a new track. I.e. the points in that track will be drawn as an own polyline */ - @UiThread fun startNewTrack(record: Boolean) { - animation.cancel() - oldTracks.add(track.trackpoints) - track = Track(ArrayList(), record) - updateOldTracks() - trackSource.clear() - trackAnimationSource.clear() - } - - /** Set all the tracks (when re-initializing) */ - @UiThread fun setTracks(pointsList: List>, isRecording: Boolean) { - require(pointsList.isNotEmpty()) - oldTracks = pointsList.map { it.toMutableList() }.toMutableList() - track = Track(ArrayList(), isRecording) - updateOldTracks() - } - - @UiThread fun clear() { - animation.cancel() - oldTracks.clear() - track = Track(ArrayList(), false) - trackSource.clear() - oldTrackSource.clear() - trackAnimationSource.clear() - } - - private fun updateAnimatedTrack(progress: Float) { - val size = track.trackpoints.size - if (size < 2) return - val s = track.trackpoints[size - 2] - val e = track.trackpoints[size - 1] - val animated = LatLon( - latitude = s.latitude + (e.latitude - s.latitude) * progress, - longitude = normalizeLongitude(s.longitude + (e.longitude - s.longitude) * progress) - ) - trackAnimationSource.setGeoJson(listOf(s, animated).toLineFeature(track.isRecording)) - } - - private fun updateTrack() { - val trackpoints = track.trackpoints - val pointsExceptLast = trackpoints.subList(0, max(0, trackpoints.size - 1)) - trackSource.setGeoJson(pointsExceptLast.toLineFeature(track.isRecording)) - if (trackpoints.size >= 2) { - animation.start() - } - } - - private fun updateOldTracks() { - val features = oldTracks.map { it.toLineFeature(false) } - oldTrackSource.setGeoJson(FeatureCollection.fromFeatures(features)) - } -} - -private fun List.toLineFeature(record: Boolean): Feature { - val line = LineString.fromLngLats(map { Point.fromLngLat(it.longitude, it.latitude) }) - val p = JsonObject() - p.addProperty("recording", record) - return Feature.fromGeometry(line, p) -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/CameraUtils.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/CameraUtils.kt deleted file mode 100644 index d86213abeeb..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/CameraUtils.kt +++ /dev/null @@ -1,72 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.maplibre - -import android.content.ContentResolver -import android.provider.Settings -import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import org.maplibre.android.camera.CameraUpdateFactory -import org.maplibre.android.maps.MapLibreMap - -fun MapLibreMap.getEnclosingCamera(geometry: ElementGeometry, padding: Padding?): CameraPosition? = - getCameraForGeometry( - geometry.toMapLibreGeometry(), - padding?.toIntArray() ?: intArrayOf(0, 0, 0, 0) - )?.toCameraPosition() - -fun MapLibreMap.getEnclosingCamera(bbox: BoundingBox, padding: Padding): CameraPosition? = - getCameraForLatLngBounds( - bbox.toLatLngBounds(), - padding.toIntArray() - )?.toCameraPosition() - -var MapLibreMap.camera: CameraPosition - get() = cameraPosition.toCameraPosition() - set(value) { cameraPosition = value.toMapLibreCameraPosition() } - -fun MapLibreMap.updateCamera(duration: Int = 0, contentResolver: ContentResolver, builder: CameraUpdate.() -> Unit) { - val update = CameraUpdate().apply(builder).toMapLibreCameraUpdate(camera) - val animatorScale = Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) - if (duration == 0 || animatorScale == 0f) { - moveCamera(update) - } else { - easeCamera(update, (duration * animatorScale).toInt()) - } -} - -private fun CameraUpdate.toMapLibreCameraUpdate(cameraPosition: CameraPosition): org.maplibre.android.camera.CameraUpdate { - resolveDeltas(cameraPosition) - val builder = org.maplibre.android.camera.CameraPosition.Builder(cameraPosition.toMapLibreCameraPosition()) - rotation?.let { builder.bearing(it) } - position?.let { builder.target(it.toLatLng()) } - zoom?.let { builder.zoom(it) } - tilt?.let { builder.tilt(it) } - padding?.let { builder.padding(it.toDoubleArray()) } - return CameraUpdateFactory.newCameraPosition(builder.build()) -} - -private fun CameraUpdate.resolveDeltas(pos: CameraPosition) { - zoomBy?.let { zoom = pos.zoom + (zoom ?: 0.0) + it } - tiltBy?.let { tilt = pos.tilt + (tilt ?: 0.0) + it } - rotationBy?.let { rotation = pos.rotation + (rotation ?: 0.0) + it } -} - -private fun org.maplibre.android.camera.CameraPosition.toCameraPosition() = CameraPosition( - position = target?.toLatLon() ?: LatLon(0.0, 0.0), - rotation = bearing, - tilt = tilt, - zoom = zoom, - padding = padding?.toPadding() -) -private fun CameraPosition.toMapLibreCameraPosition(): org.maplibre.android.camera.CameraPosition = - org.maplibre.android.camera.CameraPosition.Builder() - .bearing(rotation) - .zoom(zoom) - .tilt(tilt) - .target(position.toLatLng()) - .padding(padding?.toDoubleArray()) - .build() - -private fun Padding.toDoubleArray() = doubleArrayOf(left, top, right, bottom) -private fun Padding.toIntArray() = intArrayOf(left.toInt(), top.toInt(), right.toInt(), bottom.toInt()) -private fun DoubleArray.toPadding() = Padding(this[0], this[1], this[2], this[3]) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Expression.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Expression.kt deleted file mode 100644 index a2925f102ef..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Expression.kt +++ /dev/null @@ -1,36 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.maplibre - -import org.maplibre.android.style.expressions.Expression -import org.maplibre.android.style.expressions.Expression.* -import kotlin.math.PI - -fun inMeters(expression: Expression, latitude: Double = 30.0): Expression { - // the more north you go, the smaller of an area each mercator tile actually covers - // the additional factor of 1.20 comes from a simple measuring test with a ruler on a - // smartphone screen done at approx. latitude = 0 and latitude = 70, i.e. without it, lines are - // drawn at both latitudes approximately 20% too large - val sizeFactor = kotlin.math.cos(PI * latitude / 180) * 1.2 - return interpolate( - exponential(2), zoom(), - stop(8, division(division(expression, literal(256)), literal(sizeFactor))), - stop(24, division(product(expression, literal(256)), literal(sizeFactor))) - ) -} - -fun inMeters(width: Float, latitude: Double = 30.0): Expression { - val sizeFactor = kotlin.math.cos(PI * latitude / 180) * 1.2 - return interpolate( - exponential(2), zoom(), - stop(8, width / 256 / sizeFactor), - stop(24, width * 256 / sizeFactor) - ) -} - -fun isArea(): Expression = - any(eq(geometryType(), "Polygon"), eq(geometryType(), "MultiPolygon")) - -fun isLine(): Expression = - any(eq(geometryType(), "LineString"), eq(geometryType(), "MultiLineString")) - -fun isPoint(): Expression = - any(eq(geometryType(), "Point"), eq(geometryType(), "MultiPoint")) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapImages.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapImages.kt deleted file mode 100644 index a68eb12cc0c..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapImages.kt +++ /dev/null @@ -1,51 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.maplibre - -import android.content.res.Resources -import android.graphics.Bitmap -import de.westnordost.streetcomplete.util.logs.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.maplibre.android.maps.Style - -class MapImages(private val resources: Resources, private val style: Style) { - private val images = HashSet() - private val mutex = Mutex() - - suspend fun addOnce(id: Int, createBitmap: (Int) -> Pair) = mutex.withLock { - if (id !in images) { - val name = resources.getResourceEntryName(id) - val (bitmap, sdf) = createBitmap(id) - withContext(Dispatchers.Main) { style.addImage(name, bitmap, sdf) } - images.add(id) - Log.v("MapImages", "Loaded 1 image") - } - } - - suspend fun addOnce( - ids: Collection, - createBitmap: (id: Int) -> Pair - ) = mutex.withLock { - val loadIds = ids.filter { it !in images }.toSet() - if (loadIds.isEmpty()) return@withLock - - val data = loadIds.map { - val (bitmap, sdf) = createBitmap(it) - ImageWithMetadata(resources.getResourceEntryName(it), bitmap, sdf) - } - - val sdfImages = data.filter { it.sdf }.associateTo(HashMap()) { it.name to it.bitmap } - val nonSdfImages = data.filterNot { it.sdf }.associateTo(HashMap()) { it.name to it.bitmap } - - withContext(Dispatchers.Main) { - if (nonSdfImages.isNotEmpty()) style.addImages(nonSdfImages, false) - if (sdfImages.isNotEmpty()) style.addImages(sdfImages, true) - } - - images.addAll(loadIds) - Log.v("MapImages", "Loaded ${loadIds.size} images") - } - - private data class ImageWithMetadata(val name: String, val bitmap: Bitmap, val sdf: Boolean) -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapLibreMap.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapLibreMap.kt deleted file mode 100644 index b5573196dc0..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapLibreMap.kt +++ /dev/null @@ -1,33 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.maplibre - -import android.graphics.PointF -import android.graphics.RectF -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.Style -import org.maplibre.geojson.Feature -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -suspend fun MapLibreMap.awaitSetStyle(builder: Style.Builder): Style = suspendCoroutine { cont -> - setStyle(builder) { cont.resume(it) } -} - -fun MapLibreMap.queryRenderedFeatures( - coordinates: PointF, - radius: Float, - vararg layerIds: String -): List { - // first try without radius. If nothing found, only then expand search area - var result = queryRenderedFeatures(coordinates, *layerIds) - if (result.isNotEmpty()) return result - // then, try with small radius.... - result = queryRenderedFeatures(coordinates.expandBy(radius / 2), *layerIds) - if (result.isNotEmpty()) return result - - return queryRenderedFeatures(coordinates.expandBy(radius), *layerIds) - - // this is kind of a workaround. We'd need is this function implemented in MapLibre proper. See - // https://github.com/maplibre/maplibre-native/issues/2781 -} - -private fun PointF.expandBy(a: Float) = RectF(x - a, y - a, x + a, y + a) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapView.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapView.kt deleted file mode 100644 index 1bb04daf464..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/MapView.kt +++ /dev/null @@ -1,10 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.maplibre - -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.MapView -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -suspend fun MapView.awaitGetMap(): MapLibreMap = suspendCoroutine { cont -> - getMapAsync { cont.resume(it) } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/OfflineManager.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/OfflineManager.kt deleted file mode 100644 index 6062ca26a86..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/OfflineManager.kt +++ /dev/null @@ -1,112 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.maplibre - -import org.maplibre.android.offline.OfflineManager -import org.maplibre.android.offline.OfflineRegion -import org.maplibre.android.offline.OfflineRegionDefinition -import org.maplibre.android.offline.OfflineRegionError -import org.maplibre.android.offline.OfflineRegionStatus -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -class OfflineManagerException(message: String) : Exception(message) - -suspend fun OfflineManager.awaitGetOfflineRegions(): List = suspendCoroutine { cont -> - listOfflineRegions(object : OfflineManager.ListOfflineRegionsCallback { - override fun onError(error: String) { - cont.resumeWithException(OfflineManagerException(error)) - } - - override fun onList(offlineRegions: Array?) { - cont.resume(offlineRegions?.toList().orEmpty()) - } - }) -} - -suspend fun OfflineManager.awaitCreateOfflineRegion( - definition: OfflineRegionDefinition, - metadata: ByteArray -): OfflineRegion = suspendCoroutine { cont -> - createOfflineRegion(definition, metadata, object : OfflineManager.CreateOfflineRegionCallback { - override fun onCreate(offlineRegion: OfflineRegion) { - cont.resume(offlineRegion) - } - - override fun onError(error: String) { - cont.resumeWithException(OfflineManagerException(error)) - } - }) -} - -suspend fun OfflineManager.awaitResetDatabase(): Unit = suspendCoroutine { cont -> - resetDatabase(object : OfflineManager.FileSourceCallback { - override fun onError(message: String) { - cont.resumeWithException(OfflineManagerException(message)) - } - - override fun onSuccess() { - cont.resume(Unit) - } - }) -} - -suspend fun OfflineRegion.awaitDelete(): Unit = suspendCoroutine { cont -> - delete(object : OfflineRegion.OfflineRegionDeleteCallback { - override fun onDelete() { - cont.resume(Unit) - } - - override fun onError(error: String) { - cont.resumeWithException(OfflineManagerException(error)) - } - }) -} - -suspend fun OfflineRegion.awaitDownload(): OfflineRegionStatus = suspendCoroutine { cont -> - var observing = true - - fun shouldContinue(): Boolean = synchronized(this) { - if (!observing) return false - observing = false - return true - } - - setObserver(object : OfflineRegion.OfflineRegionObserver { - override fun mapboxTileCountLimitExceeded(limit: Long) { - if (!shouldContinue()) return - setObserver(null) - setDownloadState(OfflineRegion.STATE_INACTIVE) - cont.resumeWithException(OfflineManagerException("Tile count limit of $limit tiles exceeded")) - } - - override fun onError(error: OfflineRegionError) { - if (!shouldContinue()) return - setObserver(null) - setDownloadState(OfflineRegion.STATE_INACTIVE) - cont.resumeWithException(OfflineManagerException(error.message)) - } - - override fun onStatusChanged(status: OfflineRegionStatus) { - if (status.isComplete) { - if (!shouldContinue()) return - setObserver(null) - setDownloadState(OfflineRegion.STATE_INACTIVE) - cont.resume(status) - } - } - }) - setDownloadState(OfflineRegion.STATE_ACTIVE) -} - -/** - * Delete regions, which allows contained tiles to be deleted if cache size is exceeded. - * Assumes that download date is stored as string in [OfflineRegion.metadata]. - */ -suspend fun OfflineManager.deleteRegionsOlderThan(olderThan: Long) { - for (offlineRegion in awaitGetOfflineRegions()) { - val timestamp = offlineRegion.metadata.toString(Charsets.UTF_8).toLongOrNull() ?: 0 - if (timestamp < olderThan) { - offlineRegion.awaitDelete() - } - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Position.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Position.kt deleted file mode 100644 index b3ade64b323..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Position.kt +++ /dev/null @@ -1,100 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.maplibre - -import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry -import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry -import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry -import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.util.math.isInPolygon -import de.westnordost.streetcomplete.util.math.isRingDefinedClockwise -import de.westnordost.streetcomplete.util.math.measuredArea -import org.maplibre.android.geometry.LatLng -import org.maplibre.android.geometry.LatLngBounds -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.geojson.FeatureCollection -import org.maplibre.geojson.Geometry -import org.maplibre.geojson.LineString -import org.maplibre.geojson.MultiLineString -import org.maplibre.geojson.MultiPolygon -import org.maplibre.geojson.Point -import org.maplibre.geojson.Polygon - -fun MapLibreMap.getMetersPerPixel(): Double? = - cameraPosition.target?.latitude?.let { projection.getMetersPerPixelAtLatitude(it) } - -fun MapLibreMap.screenAreaToBoundingBox(): BoundingBox = - projection.getVisibleRegion(true).latLngBounds.toBoundingBox() - -fun LatLngBounds.toBoundingBox() = - BoundingBox(latitudeSouth, longitudeWest, latitudeNorth, longitudeEast) - -fun BoundingBox.toLatLngBounds() = - LatLngBounds.from(max.latitude, max.longitude, min.latitude, min.longitude) - -fun LatLng.toLatLon() = LatLon(latitude, longitude) -fun LatLon.toLatLng() = LatLng(latitude, longitude) - -private fun ElementGeometry.toType(): String = when (this) { - is ElementPointGeometry -> "point" - is ElementPolygonsGeometry -> "polygon" - is ElementPolylinesGeometry -> "line" -} - -fun ElementGeometry.toMapLibreGeometry(): Geometry = when (this) { - is ElementPointGeometry -> toMapLibreGeometry() - is ElementPolygonsGeometry -> toMapLibreGeometry() - is ElementPolylinesGeometry -> toMapLibreGeometry() -} - -fun ElementPointGeometry.toMapLibreGeometry(): Point = - Point.fromLngLat(center.longitude, center.latitude) - -fun ElementPolylinesGeometry.toMapLibreGeometry(): Geometry = - if (polylines.size == 1) { - LineString.fromLngLats(polylines.single().map { it.toPoint() }) - } else { - MultiLineString.fromLngLats(polylines.map { polyline -> polyline.map { it.toPoint() } }) - } - -fun ElementPolygonsGeometry.toMapLibreGeometry(): Geometry { - val outerRings = mutableListOf>() - val innerRings = mutableListOf>() - if (polygons.size == 1) { - outerRings.add(polygons.first()) - } else { - polygons.forEach { - if (it.isRingDefinedClockwise()) innerRings.add(it) else outerRings.add(it) - } - } - - if (outerRings.size == 1) { - return Polygon.fromLngLats( - (outerRings + innerRings).map { ring -> ring.map { it.toPoint() } } - ) - } - - // outerRings must be sorted size ascending to correctly handle outer rings within holes - // of larger polygons. - outerRings.sortBy { it.measuredArea() } - - // we need to allocate the holes to the different outer polygons - val groupedRings = outerRings.map { outerRing -> - val rings = mutableListOf>() - rings.add(outerRing.map { it.toPoint() }) - for (innerRing in innerRings.toList()) { - if (innerRing[0].isInPolygon(outerRing)) { - innerRings.remove(innerRing) - rings.add(innerRing.map { it.toPoint() }) - } - } - rings - } - return MultiPolygon.fromLngLats(groupedRings) -} - -fun LatLon.toPoint(): Point = Point.fromLngLat(longitude, latitude) -fun Point.toLatLon(): LatLon = LatLon(latitude(), longitude()) - -fun GeoJsonSource.clear() = setGeoJson(null as FeatureCollection?) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.android.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.android.kt new file mode 100644 index 00000000000..a8ca7d701a8 --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.android.kt @@ -0,0 +1,26 @@ +package de.westnordost.streetcomplete.ui.util + +import android.graphics.BitmapShader +import android.graphics.Shader.TileMode.CLAMP +import android.graphics.RuntimeShader +import android.os.Build +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.nativeCanvas + +actual fun createDilateShader(inputImage: ImageBitmap, radius: Float, color: Color, canvas: Canvas): Shader? { + // TODO maplibre-compose: Restore shader halos after upgrading to a release containing + // https://github.com/maplibre/maplibre-compose/pull/1394. + // Below API 33, RuntimeShader is unavailable; draw pins without halos. + if (!canvas.nativeCanvas.isHardwareAccelerated || Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return null + } + val bitmapShader = BitmapShader(inputImage.asAndroidBitmap(), CLAMP, CLAMP) + val dilateShader = RuntimeShader(createDilateSksl(radius)) + dilateShader.setInputShader("inputImage", bitmapShader) + dilateShader.setFloatUniform("dilationColor", color.red, color.green, color.blue, color.alpha) + return dilateShader +} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.android.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.android.kt deleted file mode 100644 index 93531f68f38..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.android.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.westnordost.streetcomplete.ui.util - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import org.maplibre.compose.location.LocationProvider -import org.maplibre.compose.location.createDefaultLocationProvider - -@Composable -public actual fun rememberDefaultLocationProvider(): LocationProvider { - val context = LocalContext.current - val provider = remember(context) { createDefaultLocationProvider(context) } - DisposableEffect(provider) { onDispose { provider.close() } } - return provider -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Activity.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Activity.kt deleted file mode 100644 index 26b614becd8..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Activity.kt +++ /dev/null @@ -1,19 +0,0 @@ -package de.westnordost.streetcomplete.util.ktx - -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.launch - -fun AppCompatActivity.observe(flow: Flow, collector: FlowCollector) { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - flow.collect { - collector.emit(it) - } - } - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Bitmap.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Bitmap.kt deleted file mode 100644 index 5391fbc3830..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Bitmap.kt +++ /dev/null @@ -1,15 +0,0 @@ -package de.westnordost.streetcomplete.util.ktx - -import android.graphics.Bitmap -import de.westnordost.streetcomplete.util.sdf.convertToSdf -import kotlin.math.ceil - -fun Bitmap.toSdf(radius: Double = 8.0, cutoff: Double = 0.25): Bitmap { - val buffer = ceil(radius * (1.0 - cutoff)).toInt() - val w = width + 2 * buffer - val h = height + 2 * buffer - val pixels = IntArray(w * h) - getPixels(pixels, w * buffer + buffer, w, 0, 0, width, height) - convertToSdf(pixels, w, radius, cutoff) - return Bitmap.createBitmap(pixels, w, pixels.size / w, Bitmap.Config.ARGB_8888) -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Drawable.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Drawable.kt deleted file mode 100644 index ee2f52a7f34..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Drawable.kt +++ /dev/null @@ -1,12 +0,0 @@ -package de.westnordost.streetcomplete.util.ktx - -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import androidx.core.graphics.applyCanvas -import androidx.core.graphics.createBitmap - -fun Drawable.createBitmap(width: Int = intrinsicWidth, height: Int = intrinsicHeight): Bitmap = - createBitmap(width, height, Bitmap.Config.ARGB_8888).applyCanvas { - setBounds(0, 0, this.width, this.height) - draw(this) - } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Fragment.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Fragment.kt deleted file mode 100644 index f87dd9dda79..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Fragment.kt +++ /dev/null @@ -1,21 +0,0 @@ -package de.westnordost.streetcomplete.util.ktx - -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.launch - -val Fragment.viewLifecycleScope get() = viewLifecycleOwner.lifecycleScope - -fun Fragment.observe(flow: SharedFlow, collector: FlowCollector) { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - flow.collect { - collector.emit(it) - } - } - } -} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Resources.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Resources.kt deleted file mode 100644 index 513ec363c19..00000000000 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/ktx/Resources.kt +++ /dev/null @@ -1,7 +0,0 @@ -package de.westnordost.streetcomplete.util.ktx - -import android.content.res.Resources -import androidx.core.util.TypedValueCompat - -/** return the number of pixels for the given density independent pixels */ -fun Resources.dpToPx(dp: Number): Float = TypedValueCompat.dpToPx(dp.toFloat(), displayMetrics) diff --git a/app/src/androidMain/res/values/colors.xml b/app/src/androidMain/res/values/colors.xml index 5d05786712d..c5d18c71ebf 100644 --- a/app/src/androidMain/res/values/colors.xml +++ b/app/src/androidMain/res/values/colors.xml @@ -7,6 +7,4 @@ #fff #fff #fff - - #536dfe diff --git a/app/src/commonMain/composeResources/drawable/location_shadow.xml b/app/src/commonMain/composeResources/drawable/location_shadow.xml index ce893af6b46..528ca1e687a 100644 --- a/app/src/commonMain/composeResources/drawable/location_shadow.xml +++ b/app/src/commonMain/composeResources/drawable/location_shadow.xml @@ -1,16 +1,22 @@ - - - - - - - - - + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/map_oneway_arrow.xml b/app/src/commonMain/composeResources/drawable/map_oneway_arrow.xml new file mode 100644 index 00000000000..cd52667d0f5 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/map_oneway_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/0-255.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/0-255.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/0-255.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/0-255.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/1024-1279.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/1024-1279.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/1024-1279.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/1024-1279.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/10240-10495.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/10240-10495.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/10240-10495.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/10240-10495.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/10496-10751.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/10496-10751.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/10496-10751.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/10496-10751.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/10752-11007.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/10752-11007.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/10752-11007.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/10752-11007.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/11008-11263.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/11008-11263.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/11008-11263.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/11008-11263.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/11264-11519.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/11264-11519.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/11264-11519.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/11264-11519.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/11520-11775.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/11520-11775.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/11520-11775.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/11520-11775.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/11776-12031.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/11776-12031.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/11776-12031.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/11776-12031.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/12032-12287.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/12032-12287.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/12032-12287.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/12032-12287.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/12288-12543.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/12288-12543.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/12288-12543.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/12288-12543.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/12544-12799.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/12544-12799.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/12544-12799.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/12544-12799.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/1280-1535.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/1280-1535.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/1280-1535.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/1280-1535.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/12800-13055.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/12800-13055.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/12800-13055.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/12800-13055.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/13056-13311.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/13056-13311.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/13056-13311.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/13056-13311.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/13312-13567.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/13312-13567.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/13312-13567.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/13312-13567.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/13568-13823.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/13568-13823.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/13568-13823.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/13568-13823.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/13824-14079.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/13824-14079.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/13824-14079.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/13824-14079.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/14080-14335.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/14080-14335.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/14080-14335.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/14080-14335.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/14336-14591.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/14336-14591.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/14336-14591.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/14336-14591.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/14592-14847.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/14592-14847.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/14592-14847.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/14592-14847.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/14848-15103.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/14848-15103.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/14848-15103.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/14848-15103.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/15104-15359.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/15104-15359.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/15104-15359.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/15104-15359.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/1536-1791.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/1536-1791.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/1536-1791.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/1536-1791.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/15360-15615.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/15360-15615.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/15360-15615.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/15360-15615.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/15616-15871.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/15616-15871.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/15616-15871.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/15616-15871.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/15872-16127.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/15872-16127.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/15872-16127.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/15872-16127.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/16128-16383.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/16128-16383.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/16128-16383.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/16128-16383.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/16384-16639.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/16384-16639.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/16384-16639.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/16384-16639.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/16640-16895.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/16640-16895.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/16640-16895.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/16640-16895.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/16896-17151.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/16896-17151.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/16896-17151.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/16896-17151.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/17152-17407.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/17152-17407.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/17152-17407.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/17152-17407.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/17408-17663.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/17408-17663.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/17408-17663.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/17408-17663.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/17664-17919.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/17664-17919.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/17664-17919.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/17664-17919.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/1792-2047.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/1792-2047.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/1792-2047.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/1792-2047.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/17920-18175.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/17920-18175.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/17920-18175.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/17920-18175.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/18176-18431.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/18176-18431.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/18176-18431.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/18176-18431.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/18432-18687.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/18432-18687.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/18432-18687.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/18432-18687.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/18688-18943.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/18688-18943.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/18688-18943.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/18688-18943.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/18944-19199.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/18944-19199.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/18944-19199.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/18944-19199.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/19200-19455.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/19200-19455.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/19200-19455.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/19200-19455.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/19456-19711.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/19456-19711.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/19456-19711.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/19456-19711.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/19712-19967.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/19712-19967.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/19712-19967.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/19712-19967.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/19968-20223.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/19968-20223.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/19968-20223.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/19968-20223.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/20224-20479.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/20224-20479.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/20224-20479.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/20224-20479.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/2048-2303.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/2048-2303.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/2048-2303.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/2048-2303.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/20480-20735.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/20480-20735.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/20480-20735.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/20480-20735.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/20736-20991.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/20736-20991.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/20736-20991.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/20736-20991.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/20992-21247.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/20992-21247.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/20992-21247.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/20992-21247.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/21248-21503.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/21248-21503.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/21248-21503.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/21248-21503.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/21504-21759.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/21504-21759.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/21504-21759.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/21504-21759.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/21760-22015.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/21760-22015.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/21760-22015.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/21760-22015.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/22016-22271.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/22016-22271.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/22016-22271.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/22016-22271.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/22272-22527.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/22272-22527.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/22272-22527.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/22272-22527.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/22528-22783.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/22528-22783.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/22528-22783.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/22528-22783.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/22784-23039.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/22784-23039.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/22784-23039.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/22784-23039.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/2304-2559.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/2304-2559.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/2304-2559.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/2304-2559.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/23040-23295.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/23040-23295.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/23040-23295.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/23040-23295.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/23296-23551.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/23296-23551.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/23296-23551.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/23296-23551.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/23552-23807.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/23552-23807.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/23552-23807.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/23552-23807.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/23808-24063.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/23808-24063.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/23808-24063.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/23808-24063.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/24064-24319.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/24064-24319.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/24064-24319.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/24064-24319.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/24320-24575.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/24320-24575.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/24320-24575.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/24320-24575.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/24576-24831.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/24576-24831.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/24576-24831.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/24576-24831.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/24832-25087.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/24832-25087.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/24832-25087.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/24832-25087.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/25088-25343.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/25088-25343.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/25088-25343.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/25088-25343.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/25344-25599.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/25344-25599.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/25344-25599.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/25344-25599.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/256-511.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/256-511.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/256-511.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/256-511.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/2560-2815.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/2560-2815.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/2560-2815.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/2560-2815.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/25600-25855.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/25600-25855.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/25600-25855.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/25600-25855.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/25856-26111.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/25856-26111.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/25856-26111.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/25856-26111.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/26112-26367.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/26112-26367.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/26112-26367.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/26112-26367.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/26368-26623.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/26368-26623.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/26368-26623.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/26368-26623.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/26624-26879.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/26624-26879.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/26624-26879.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/26624-26879.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/26880-27135.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/26880-27135.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/26880-27135.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/26880-27135.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/27136-27391.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/27136-27391.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/27136-27391.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/27136-27391.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/27392-27647.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/27392-27647.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/27392-27647.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/27392-27647.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/27648-27903.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/27648-27903.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/27648-27903.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/27648-27903.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/27904-28159.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/27904-28159.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/27904-28159.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/27904-28159.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/2816-3071.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/2816-3071.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/2816-3071.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/2816-3071.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/28160-28415.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/28160-28415.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/28160-28415.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/28160-28415.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/28416-28671.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/28416-28671.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/28416-28671.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/28416-28671.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/28672-28927.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/28672-28927.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/28672-28927.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/28672-28927.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/28928-29183.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/28928-29183.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/28928-29183.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/28928-29183.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/29184-29439.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/29184-29439.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/29184-29439.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/29184-29439.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/29440-29695.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/29440-29695.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/29440-29695.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/29440-29695.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/29696-29951.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/29696-29951.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/29696-29951.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/29696-29951.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/29952-30207.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/29952-30207.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/29952-30207.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/29952-30207.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/30208-30463.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/30208-30463.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/30208-30463.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/30208-30463.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/30464-30719.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/30464-30719.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/30464-30719.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/30464-30719.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/3072-3327.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/3072-3327.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/3072-3327.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/3072-3327.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/30720-30975.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/30720-30975.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/30720-30975.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/30720-30975.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/30976-31231.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/30976-31231.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/30976-31231.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/30976-31231.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/31232-31487.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/31232-31487.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/31232-31487.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/31232-31487.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/31488-31743.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/31488-31743.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/31488-31743.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/31488-31743.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/31744-31999.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/31744-31999.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/31744-31999.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/31744-31999.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/32000-32255.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/32000-32255.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/32000-32255.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/32000-32255.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/32256-32511.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/32256-32511.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/32256-32511.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/32256-32511.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/32512-32767.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/32512-32767.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/32512-32767.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/32512-32767.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/32768-33023.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/32768-33023.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/32768-33023.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/32768-33023.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/33024-33279.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/33024-33279.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/33024-33279.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/33024-33279.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/3328-3583.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/3328-3583.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/3328-3583.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/3328-3583.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/33280-33535.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/33280-33535.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/33280-33535.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/33280-33535.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/33536-33791.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/33536-33791.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/33536-33791.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/33536-33791.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/33792-34047.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/33792-34047.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/33792-34047.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/33792-34047.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/34048-34303.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/34048-34303.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/34048-34303.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/34048-34303.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/34304-34559.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/34304-34559.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/34304-34559.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/34304-34559.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/34560-34815.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/34560-34815.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/34560-34815.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/34560-34815.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/34816-35071.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/34816-35071.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/34816-35071.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/34816-35071.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/35072-35327.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/35072-35327.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/35072-35327.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/35072-35327.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/35328-35583.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/35328-35583.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/35328-35583.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/35328-35583.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/35584-35839.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/35584-35839.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/35584-35839.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/35584-35839.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/3584-3839.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/3584-3839.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/3584-3839.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/3584-3839.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/35840-36095.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/35840-36095.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/35840-36095.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/35840-36095.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/36096-36351.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/36096-36351.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/36096-36351.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/36096-36351.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/36352-36607.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/36352-36607.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/36352-36607.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/36352-36607.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/36608-36863.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/36608-36863.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/36608-36863.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/36608-36863.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/36864-37119.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/36864-37119.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/36864-37119.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/36864-37119.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/37120-37375.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/37120-37375.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/37120-37375.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/37120-37375.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/37376-37631.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/37376-37631.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/37376-37631.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/37376-37631.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/37632-37887.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/37632-37887.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/37632-37887.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/37632-37887.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/37888-38143.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/37888-38143.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/37888-38143.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/37888-38143.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/38144-38399.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/38144-38399.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/38144-38399.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/38144-38399.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/3840-4095.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/3840-4095.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/3840-4095.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/3840-4095.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/38400-38655.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/38400-38655.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/38400-38655.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/38400-38655.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/38656-38911.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/38656-38911.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/38656-38911.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/38656-38911.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/38912-39167.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/38912-39167.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/38912-39167.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/38912-39167.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/39168-39423.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/39168-39423.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/39168-39423.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/39168-39423.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/39424-39679.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/39424-39679.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/39424-39679.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/39424-39679.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/39680-39935.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/39680-39935.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/39680-39935.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/39680-39935.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/39936-40191.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/39936-40191.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/39936-40191.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/39936-40191.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/40192-40447.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/40192-40447.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/40192-40447.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/40192-40447.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/40448-40703.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/40448-40703.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/40448-40703.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/40448-40703.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/40704-40959.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/40704-40959.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/40704-40959.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/40704-40959.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/4096-4351.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/4096-4351.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/4096-4351.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/4096-4351.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/40960-41215.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/40960-41215.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/40960-41215.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/40960-41215.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/41216-41471.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/41216-41471.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/41216-41471.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/41216-41471.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/41472-41727.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/41472-41727.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/41472-41727.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/41472-41727.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/41728-41983.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/41728-41983.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/41728-41983.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/41728-41983.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/41984-42239.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/41984-42239.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/41984-42239.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/41984-42239.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/42240-42495.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/42240-42495.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/42240-42495.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/42240-42495.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/42496-42751.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/42496-42751.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/42496-42751.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/42496-42751.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/42752-43007.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/42752-43007.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/42752-43007.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/42752-43007.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/43008-43263.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/43008-43263.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/43008-43263.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/43008-43263.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/43264-43519.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/43264-43519.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/43264-43519.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/43264-43519.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/4352-4607.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/4352-4607.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/4352-4607.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/4352-4607.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/43520-43775.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/43520-43775.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/43520-43775.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/43520-43775.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/43776-44031.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/43776-44031.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/43776-44031.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/43776-44031.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/44032-44287.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/44032-44287.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/44032-44287.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/44032-44287.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/44288-44543.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/44288-44543.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/44288-44543.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/44288-44543.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/44544-44799.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/44544-44799.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/44544-44799.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/44544-44799.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/44800-45055.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/44800-45055.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/44800-45055.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/44800-45055.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/45056-45311.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/45056-45311.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/45056-45311.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/45056-45311.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/45312-45567.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/45312-45567.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/45312-45567.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/45312-45567.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/45568-45823.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/45568-45823.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/45568-45823.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/45568-45823.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/45824-46079.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/45824-46079.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/45824-46079.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/45824-46079.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/4608-4863.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/4608-4863.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/4608-4863.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/4608-4863.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/46080-46335.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/46080-46335.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/46080-46335.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/46080-46335.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/46336-46591.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/46336-46591.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/46336-46591.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/46336-46591.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/46592-46847.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/46592-46847.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/46592-46847.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/46592-46847.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/46848-47103.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/46848-47103.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/46848-47103.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/46848-47103.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/47104-47359.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/47104-47359.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/47104-47359.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/47104-47359.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/47360-47615.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/47360-47615.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/47360-47615.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/47360-47615.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/47616-47871.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/47616-47871.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/47616-47871.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/47616-47871.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/47872-48127.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/47872-48127.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/47872-48127.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/47872-48127.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/48128-48383.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/48128-48383.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/48128-48383.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/48128-48383.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/48384-48639.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/48384-48639.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/48384-48639.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/48384-48639.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/4864-5119.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/4864-5119.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/4864-5119.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/4864-5119.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/48640-48895.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/48640-48895.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/48640-48895.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/48640-48895.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/48896-49151.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/48896-49151.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/48896-49151.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/48896-49151.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/49152-49407.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/49152-49407.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/49152-49407.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/49152-49407.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/49408-49663.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/49408-49663.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/49408-49663.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/49408-49663.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/49664-49919.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/49664-49919.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/49664-49919.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/49664-49919.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/49920-50175.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/49920-50175.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/49920-50175.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/49920-50175.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/50176-50431.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/50176-50431.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/50176-50431.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/50176-50431.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/50432-50687.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/50432-50687.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/50432-50687.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/50432-50687.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/50688-50943.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/50688-50943.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/50688-50943.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/50688-50943.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/50944-51199.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/50944-51199.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/50944-51199.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/50944-51199.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/512-767.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/512-767.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/512-767.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/512-767.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/5120-5375.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/5120-5375.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/5120-5375.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/5120-5375.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/51200-51455.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/51200-51455.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/51200-51455.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/51200-51455.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/51456-51711.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/51456-51711.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/51456-51711.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/51456-51711.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/51712-51967.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/51712-51967.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/51712-51967.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/51712-51967.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/51968-52223.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/51968-52223.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/51968-52223.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/51968-52223.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/52224-52479.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/52224-52479.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/52224-52479.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/52224-52479.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/52480-52735.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/52480-52735.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/52480-52735.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/52480-52735.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/52736-52991.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/52736-52991.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/52736-52991.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/52736-52991.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/52992-53247.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/52992-53247.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/52992-53247.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/52992-53247.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/53248-53503.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/53248-53503.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/53248-53503.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/53248-53503.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/53504-53759.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/53504-53759.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/53504-53759.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/53504-53759.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/5376-5631.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/5376-5631.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/5376-5631.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/5376-5631.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/53760-54015.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/53760-54015.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/53760-54015.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/53760-54015.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/54016-54271.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/54016-54271.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/54016-54271.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/54016-54271.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/54272-54527.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/54272-54527.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/54272-54527.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/54272-54527.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/54528-54783.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/54528-54783.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/54528-54783.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/54528-54783.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/54784-55039.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/54784-55039.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/54784-55039.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/54784-55039.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/55040-55295.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/55040-55295.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/55040-55295.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/55040-55295.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/55296-55551.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/55296-55551.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/55296-55551.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/55296-55551.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/55552-55807.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/55552-55807.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/55552-55807.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/55552-55807.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/55808-56063.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/55808-56063.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/55808-56063.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/55808-56063.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/56064-56319.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/56064-56319.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/56064-56319.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/56064-56319.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/5632-5887.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/5632-5887.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/5632-5887.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/5632-5887.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/56320-56575.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/56320-56575.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/56320-56575.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/56320-56575.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/56576-56831.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/56576-56831.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/56576-56831.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/56576-56831.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/56832-57087.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/56832-57087.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/56832-57087.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/56832-57087.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/57088-57343.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/57088-57343.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/57088-57343.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/57088-57343.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/57344-57599.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/57344-57599.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/57344-57599.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/57344-57599.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/57600-57855.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/57600-57855.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/57600-57855.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/57600-57855.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/57856-58111.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/57856-58111.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/57856-58111.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/57856-58111.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/58112-58367.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/58112-58367.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/58112-58367.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/58112-58367.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/58368-58623.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/58368-58623.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/58368-58623.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/58368-58623.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/58624-58879.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/58624-58879.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/58624-58879.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/58624-58879.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/5888-6143.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/5888-6143.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/5888-6143.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/5888-6143.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/58880-59135.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/58880-59135.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/58880-59135.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/58880-59135.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/59136-59391.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/59136-59391.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/59136-59391.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/59136-59391.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/59392-59647.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/59392-59647.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/59392-59647.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/59392-59647.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/59648-59903.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/59648-59903.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/59648-59903.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/59648-59903.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/59904-60159.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/59904-60159.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/59904-60159.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/59904-60159.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/60160-60415.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/60160-60415.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/60160-60415.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/60160-60415.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/60416-60671.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/60416-60671.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/60416-60671.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/60416-60671.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/60672-60927.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/60672-60927.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/60672-60927.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/60672-60927.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/60928-61183.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/60928-61183.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/60928-61183.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/60928-61183.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/61184-61439.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/61184-61439.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/61184-61439.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/61184-61439.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/6144-6399.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/6144-6399.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/6144-6399.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/6144-6399.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/61440-61695.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/61440-61695.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/61440-61695.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/61440-61695.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/61696-61951.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/61696-61951.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/61696-61951.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/61696-61951.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/61952-62207.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/61952-62207.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/61952-62207.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/61952-62207.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/62208-62463.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/62208-62463.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/62208-62463.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/62208-62463.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/62464-62719.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/62464-62719.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/62464-62719.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/62464-62719.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/62720-62975.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/62720-62975.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/62720-62975.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/62720-62975.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/62976-63231.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/62976-63231.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/62976-63231.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/62976-63231.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/63232-63487.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/63232-63487.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/63232-63487.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/63232-63487.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/63488-63743.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/63488-63743.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/63488-63743.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/63488-63743.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/63744-63999.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/63744-63999.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/63744-63999.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/63744-63999.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/6400-6655.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/6400-6655.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/6400-6655.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/6400-6655.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/64000-64255.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/64000-64255.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/64000-64255.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/64000-64255.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/64256-64511.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/64256-64511.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/64256-64511.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/64256-64511.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/64512-64767.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/64512-64767.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/64512-64767.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/64512-64767.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/64768-65023.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/64768-65023.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/64768-65023.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/64768-65023.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/65024-65279.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/65024-65279.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/65024-65279.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/65024-65279.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/65280-65535.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/65280-65535.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/65280-65535.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/65280-65535.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/6656-6911.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/6656-6911.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/6656-6911.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/6656-6911.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/6912-7167.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/6912-7167.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/6912-7167.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/6912-7167.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/7168-7423.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/7168-7423.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/7168-7423.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/7168-7423.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/7424-7679.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/7424-7679.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/7424-7679.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/7424-7679.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/768-1023.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/768-1023.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/768-1023.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/768-1023.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/7680-7935.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/7680-7935.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/7680-7935.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/7680-7935.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/7936-8191.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/7936-8191.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/7936-8191.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/7936-8191.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/8192-8447.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/8192-8447.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/8192-8447.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/8192-8447.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/8448-8703.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/8448-8703.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/8448-8703.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/8448-8703.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/8704-8959.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/8704-8959.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/8704-8959.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/8704-8959.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/8960-9215.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/8960-9215.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/8960-9215.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/8960-9215.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/9216-9471.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/9216-9471.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/9216-9471.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/9216-9471.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/9472-9727.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/9472-9727.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/9472-9727.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/9472-9727.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/9728-9983.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/9728-9983.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/9728-9983.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/9728-9983.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/9984-10239.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Bold/9984-10239.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Bold/9984-10239.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Bold/9984-10239.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/0-255.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/0-255.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/0-255.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/0-255.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/1024-1279.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/1024-1279.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/1024-1279.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/1024-1279.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/10240-10495.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/10240-10495.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/10240-10495.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/10240-10495.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/10496-10751.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/10496-10751.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/10496-10751.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/10496-10751.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/10752-11007.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/10752-11007.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/10752-11007.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/10752-11007.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/11008-11263.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/11008-11263.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/11008-11263.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/11008-11263.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/11264-11519.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/11264-11519.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/11264-11519.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/11264-11519.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/11520-11775.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/11520-11775.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/11520-11775.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/11520-11775.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/11776-12031.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/11776-12031.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/11776-12031.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/11776-12031.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/12032-12287.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/12032-12287.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/12032-12287.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/12032-12287.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/12288-12543.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/12288-12543.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/12288-12543.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/12288-12543.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/12544-12799.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/12544-12799.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/12544-12799.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/12544-12799.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/1280-1535.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/1280-1535.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/1280-1535.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/1280-1535.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/12800-13055.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/12800-13055.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/12800-13055.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/12800-13055.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/13056-13311.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/13056-13311.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/13056-13311.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/13056-13311.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/13312-13567.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/13312-13567.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/13312-13567.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/13312-13567.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/13568-13823.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/13568-13823.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/13568-13823.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/13568-13823.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/13824-14079.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/13824-14079.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/13824-14079.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/13824-14079.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/14080-14335.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/14080-14335.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/14080-14335.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/14080-14335.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/14336-14591.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/14336-14591.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/14336-14591.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/14336-14591.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/14592-14847.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/14592-14847.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/14592-14847.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/14592-14847.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/14848-15103.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/14848-15103.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/14848-15103.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/14848-15103.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/15104-15359.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/15104-15359.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/15104-15359.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/15104-15359.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/1536-1791.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/1536-1791.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/1536-1791.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/1536-1791.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/15360-15615.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/15360-15615.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/15360-15615.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/15360-15615.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/15616-15871.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/15616-15871.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/15616-15871.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/15616-15871.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/15872-16127.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/15872-16127.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/15872-16127.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/15872-16127.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/16128-16383.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/16128-16383.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/16128-16383.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/16128-16383.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/16384-16639.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/16384-16639.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/16384-16639.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/16384-16639.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/16640-16895.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/16640-16895.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/16640-16895.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/16640-16895.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/16896-17151.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/16896-17151.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/16896-17151.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/16896-17151.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/17152-17407.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/17152-17407.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/17152-17407.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/17152-17407.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/17408-17663.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/17408-17663.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/17408-17663.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/17408-17663.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/17664-17919.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/17664-17919.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/17664-17919.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/17664-17919.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/1792-2047.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/1792-2047.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/1792-2047.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/1792-2047.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/17920-18175.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/17920-18175.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/17920-18175.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/17920-18175.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/18176-18431.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/18176-18431.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/18176-18431.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/18176-18431.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/18432-18687.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/18432-18687.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/18432-18687.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/18432-18687.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/18688-18943.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/18688-18943.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/18688-18943.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/18688-18943.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/18944-19199.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/18944-19199.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/18944-19199.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/18944-19199.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/19200-19455.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/19200-19455.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/19200-19455.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/19200-19455.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/19456-19711.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/19456-19711.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/19456-19711.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/19456-19711.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/19712-19967.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/19712-19967.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/19712-19967.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/19712-19967.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/19968-20223.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/19968-20223.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/19968-20223.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/19968-20223.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/20224-20479.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/20224-20479.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/20224-20479.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/20224-20479.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/2048-2303.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/2048-2303.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/2048-2303.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/2048-2303.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/20480-20735.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/20480-20735.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/20480-20735.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/20480-20735.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/20736-20991.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/20736-20991.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/20736-20991.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/20736-20991.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/20992-21247.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/20992-21247.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/20992-21247.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/20992-21247.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/21248-21503.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/21248-21503.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/21248-21503.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/21248-21503.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/21504-21759.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/21504-21759.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/21504-21759.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/21504-21759.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/21760-22015.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/21760-22015.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/21760-22015.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/21760-22015.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/22016-22271.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/22016-22271.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/22016-22271.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/22016-22271.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/22272-22527.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/22272-22527.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/22272-22527.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/22272-22527.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/22528-22783.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/22528-22783.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/22528-22783.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/22528-22783.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/22784-23039.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/22784-23039.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/22784-23039.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/22784-23039.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/2304-2559.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/2304-2559.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/2304-2559.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/2304-2559.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/23040-23295.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/23040-23295.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/23040-23295.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/23040-23295.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/23296-23551.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/23296-23551.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/23296-23551.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/23296-23551.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/23552-23807.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/23552-23807.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/23552-23807.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/23552-23807.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/23808-24063.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/23808-24063.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/23808-24063.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/23808-24063.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/24064-24319.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/24064-24319.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/24064-24319.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/24064-24319.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/24320-24575.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/24320-24575.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/24320-24575.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/24320-24575.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/24576-24831.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/24576-24831.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/24576-24831.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/24576-24831.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/24832-25087.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/24832-25087.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/24832-25087.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/24832-25087.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/25088-25343.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/25088-25343.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/25088-25343.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/25088-25343.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/25344-25599.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/25344-25599.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/25344-25599.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/25344-25599.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/256-511.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/256-511.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/256-511.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/256-511.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/2560-2815.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/2560-2815.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/2560-2815.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/2560-2815.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/25600-25855.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/25600-25855.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/25600-25855.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/25600-25855.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/25856-26111.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/25856-26111.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/25856-26111.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/25856-26111.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/26112-26367.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/26112-26367.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/26112-26367.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/26112-26367.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/26368-26623.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/26368-26623.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/26368-26623.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/26368-26623.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/26624-26879.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/26624-26879.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/26624-26879.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/26624-26879.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/26880-27135.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/26880-27135.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/26880-27135.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/26880-27135.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/27136-27391.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/27136-27391.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/27136-27391.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/27136-27391.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/27392-27647.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/27392-27647.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/27392-27647.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/27392-27647.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/27648-27903.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/27648-27903.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/27648-27903.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/27648-27903.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/27904-28159.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/27904-28159.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/27904-28159.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/27904-28159.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/2816-3071.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/2816-3071.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/2816-3071.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/2816-3071.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/28160-28415.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/28160-28415.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/28160-28415.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/28160-28415.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/28416-28671.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/28416-28671.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/28416-28671.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/28416-28671.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/28672-28927.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/28672-28927.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/28672-28927.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/28672-28927.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/28928-29183.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/28928-29183.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/28928-29183.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/28928-29183.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/29184-29439.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/29184-29439.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/29184-29439.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/29184-29439.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/29440-29695.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/29440-29695.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/29440-29695.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/29440-29695.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/29696-29951.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/29696-29951.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/29696-29951.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/29696-29951.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/29952-30207.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/29952-30207.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/29952-30207.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/29952-30207.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/30208-30463.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/30208-30463.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/30208-30463.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/30208-30463.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/30464-30719.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/30464-30719.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/30464-30719.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/30464-30719.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/3072-3327.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/3072-3327.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/3072-3327.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/3072-3327.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/30720-30975.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/30720-30975.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/30720-30975.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/30720-30975.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/30976-31231.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/30976-31231.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/30976-31231.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/30976-31231.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/31232-31487.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/31232-31487.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/31232-31487.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/31232-31487.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/31488-31743.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/31488-31743.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/31488-31743.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/31488-31743.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/31744-31999.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/31744-31999.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/31744-31999.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/31744-31999.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/32000-32255.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/32000-32255.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/32000-32255.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/32000-32255.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/32256-32511.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/32256-32511.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/32256-32511.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/32256-32511.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/32512-32767.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/32512-32767.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/32512-32767.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/32512-32767.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/32768-33023.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/32768-33023.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/32768-33023.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/32768-33023.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/33024-33279.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/33024-33279.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/33024-33279.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/33024-33279.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/3328-3583.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/3328-3583.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/3328-3583.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/3328-3583.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/33280-33535.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/33280-33535.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/33280-33535.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/33280-33535.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/33536-33791.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/33536-33791.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/33536-33791.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/33536-33791.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/33792-34047.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/33792-34047.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/33792-34047.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/33792-34047.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/34048-34303.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/34048-34303.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/34048-34303.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/34048-34303.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/34304-34559.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/34304-34559.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/34304-34559.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/34304-34559.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/34560-34815.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/34560-34815.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/34560-34815.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/34560-34815.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/34816-35071.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/34816-35071.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/34816-35071.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/34816-35071.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/35072-35327.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/35072-35327.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/35072-35327.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/35072-35327.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/35328-35583.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/35328-35583.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/35328-35583.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/35328-35583.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/35584-35839.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/35584-35839.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/35584-35839.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/35584-35839.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/3584-3839.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/3584-3839.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/3584-3839.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/3584-3839.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/35840-36095.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/35840-36095.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/35840-36095.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/35840-36095.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/36096-36351.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/36096-36351.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/36096-36351.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/36096-36351.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/36352-36607.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/36352-36607.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/36352-36607.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/36352-36607.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/36608-36863.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/36608-36863.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/36608-36863.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/36608-36863.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/36864-37119.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/36864-37119.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/36864-37119.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/36864-37119.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/37120-37375.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/37120-37375.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/37120-37375.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/37120-37375.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/37376-37631.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/37376-37631.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/37376-37631.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/37376-37631.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/37632-37887.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/37632-37887.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/37632-37887.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/37632-37887.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/37888-38143.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/37888-38143.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/37888-38143.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/37888-38143.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/38144-38399.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/38144-38399.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/38144-38399.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/38144-38399.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/3840-4095.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/3840-4095.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/3840-4095.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/3840-4095.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/38400-38655.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/38400-38655.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/38400-38655.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/38400-38655.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/38656-38911.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/38656-38911.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/38656-38911.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/38656-38911.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/38912-39167.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/38912-39167.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/38912-39167.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/38912-39167.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/39168-39423.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/39168-39423.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/39168-39423.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/39168-39423.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/39424-39679.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/39424-39679.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/39424-39679.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/39424-39679.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/39680-39935.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/39680-39935.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/39680-39935.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/39680-39935.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/39936-40191.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/39936-40191.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/39936-40191.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/39936-40191.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/40192-40447.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/40192-40447.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/40192-40447.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/40192-40447.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/40448-40703.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/40448-40703.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/40448-40703.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/40448-40703.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/40704-40959.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/40704-40959.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/40704-40959.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/40704-40959.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/4096-4351.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/4096-4351.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/4096-4351.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/4096-4351.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/40960-41215.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/40960-41215.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/40960-41215.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/40960-41215.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/41216-41471.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/41216-41471.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/41216-41471.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/41216-41471.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/41472-41727.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/41472-41727.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/41472-41727.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/41472-41727.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/41728-41983.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/41728-41983.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/41728-41983.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/41728-41983.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/41984-42239.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/41984-42239.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/41984-42239.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/41984-42239.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/42240-42495.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/42240-42495.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/42240-42495.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/42240-42495.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/42496-42751.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/42496-42751.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/42496-42751.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/42496-42751.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/42752-43007.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/42752-43007.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/42752-43007.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/42752-43007.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/43008-43263.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/43008-43263.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/43008-43263.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/43008-43263.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/43264-43519.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/43264-43519.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/43264-43519.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/43264-43519.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/4352-4607.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/4352-4607.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/4352-4607.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/4352-4607.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/43520-43775.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/43520-43775.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/43520-43775.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/43520-43775.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/43776-44031.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/43776-44031.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/43776-44031.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/43776-44031.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/44032-44287.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/44032-44287.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/44032-44287.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/44032-44287.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/44288-44543.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/44288-44543.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/44288-44543.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/44288-44543.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/44544-44799.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/44544-44799.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/44544-44799.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/44544-44799.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/44800-45055.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/44800-45055.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/44800-45055.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/44800-45055.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/45056-45311.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/45056-45311.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/45056-45311.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/45056-45311.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/45312-45567.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/45312-45567.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/45312-45567.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/45312-45567.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/45568-45823.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/45568-45823.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/45568-45823.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/45568-45823.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/45824-46079.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/45824-46079.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/45824-46079.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/45824-46079.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/4608-4863.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/4608-4863.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/4608-4863.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/4608-4863.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/46080-46335.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/46080-46335.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/46080-46335.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/46080-46335.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/46336-46591.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/46336-46591.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/46336-46591.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/46336-46591.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/46592-46847.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/46592-46847.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/46592-46847.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/46592-46847.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/46848-47103.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/46848-47103.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/46848-47103.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/46848-47103.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/47104-47359.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/47104-47359.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/47104-47359.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/47104-47359.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/47360-47615.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/47360-47615.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/47360-47615.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/47360-47615.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/47616-47871.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/47616-47871.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/47616-47871.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/47616-47871.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/47872-48127.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/47872-48127.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/47872-48127.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/47872-48127.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/48128-48383.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/48128-48383.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/48128-48383.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/48128-48383.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/48384-48639.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/48384-48639.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/48384-48639.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/48384-48639.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/4864-5119.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/4864-5119.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/4864-5119.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/4864-5119.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/48640-48895.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/48640-48895.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/48640-48895.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/48640-48895.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/48896-49151.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/48896-49151.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/48896-49151.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/48896-49151.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/49152-49407.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/49152-49407.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/49152-49407.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/49152-49407.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/49408-49663.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/49408-49663.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/49408-49663.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/49408-49663.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/49664-49919.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/49664-49919.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/49664-49919.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/49664-49919.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/49920-50175.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/49920-50175.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/49920-50175.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/49920-50175.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/50176-50431.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/50176-50431.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/50176-50431.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/50176-50431.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/50432-50687.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/50432-50687.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/50432-50687.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/50432-50687.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/50688-50943.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/50688-50943.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/50688-50943.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/50688-50943.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/50944-51199.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/50944-51199.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/50944-51199.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/50944-51199.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/512-767.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/512-767.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/512-767.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/512-767.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/5120-5375.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/5120-5375.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/5120-5375.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/5120-5375.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/51200-51455.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/51200-51455.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/51200-51455.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/51200-51455.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/51456-51711.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/51456-51711.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/51456-51711.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/51456-51711.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/51712-51967.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/51712-51967.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/51712-51967.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/51712-51967.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/51968-52223.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/51968-52223.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/51968-52223.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/51968-52223.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/52224-52479.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/52224-52479.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/52224-52479.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/52224-52479.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/52480-52735.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/52480-52735.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/52480-52735.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/52480-52735.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/52736-52991.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/52736-52991.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/52736-52991.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/52736-52991.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/52992-53247.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/52992-53247.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/52992-53247.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/52992-53247.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/53248-53503.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/53248-53503.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/53248-53503.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/53248-53503.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/53504-53759.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/53504-53759.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/53504-53759.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/53504-53759.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/5376-5631.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/5376-5631.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/5376-5631.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/5376-5631.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/53760-54015.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/53760-54015.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/53760-54015.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/53760-54015.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/54016-54271.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/54016-54271.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/54016-54271.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/54016-54271.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/54272-54527.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/54272-54527.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/54272-54527.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/54272-54527.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/54528-54783.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/54528-54783.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/54528-54783.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/54528-54783.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/54784-55039.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/54784-55039.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/54784-55039.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/54784-55039.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/55040-55295.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/55040-55295.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/55040-55295.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/55040-55295.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/55296-55551.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/55296-55551.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/55296-55551.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/55296-55551.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/55552-55807.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/55552-55807.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/55552-55807.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/55552-55807.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/55808-56063.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/55808-56063.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/55808-56063.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/55808-56063.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/56064-56319.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/56064-56319.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/56064-56319.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/56064-56319.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/5632-5887.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/5632-5887.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/5632-5887.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/5632-5887.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/56320-56575.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/56320-56575.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/56320-56575.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/56320-56575.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/56576-56831.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/56576-56831.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/56576-56831.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/56576-56831.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/56832-57087.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/56832-57087.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/56832-57087.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/56832-57087.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/57088-57343.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/57088-57343.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/57088-57343.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/57088-57343.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/57344-57599.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/57344-57599.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/57344-57599.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/57344-57599.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/57600-57855.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/57600-57855.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/57600-57855.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/57600-57855.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/57856-58111.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/57856-58111.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/57856-58111.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/57856-58111.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/58112-58367.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/58112-58367.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/58112-58367.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/58112-58367.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/58368-58623.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/58368-58623.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/58368-58623.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/58368-58623.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/58624-58879.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/58624-58879.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/58624-58879.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/58624-58879.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/5888-6143.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/5888-6143.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/5888-6143.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/5888-6143.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/58880-59135.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/58880-59135.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/58880-59135.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/58880-59135.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/59136-59391.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/59136-59391.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/59136-59391.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/59136-59391.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/59392-59647.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/59392-59647.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/59392-59647.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/59392-59647.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/59648-59903.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/59648-59903.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/59648-59903.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/59648-59903.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/59904-60159.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/59904-60159.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/59904-60159.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/59904-60159.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/60160-60415.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/60160-60415.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/60160-60415.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/60160-60415.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/60416-60671.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/60416-60671.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/60416-60671.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/60416-60671.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/60672-60927.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/60672-60927.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/60672-60927.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/60672-60927.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/60928-61183.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/60928-61183.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/60928-61183.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/60928-61183.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/61184-61439.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/61184-61439.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/61184-61439.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/61184-61439.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/6144-6399.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/6144-6399.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/6144-6399.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/6144-6399.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/61440-61695.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/61440-61695.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/61440-61695.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/61440-61695.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/61696-61951.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/61696-61951.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/61696-61951.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/61696-61951.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/61952-62207.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/61952-62207.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/61952-62207.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/61952-62207.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/62208-62463.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/62208-62463.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/62208-62463.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/62208-62463.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/62464-62719.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/62464-62719.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/62464-62719.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/62464-62719.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/62720-62975.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/62720-62975.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/62720-62975.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/62720-62975.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/62976-63231.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/62976-63231.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/62976-63231.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/62976-63231.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/63232-63487.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/63232-63487.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/63232-63487.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/63232-63487.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/63488-63743.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/63488-63743.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/63488-63743.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/63488-63743.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/63744-63999.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/63744-63999.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/63744-63999.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/63744-63999.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/6400-6655.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/6400-6655.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/6400-6655.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/6400-6655.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/64000-64255.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/64000-64255.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/64000-64255.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/64000-64255.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/64256-64511.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/64256-64511.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/64256-64511.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/64256-64511.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/64512-64767.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/64512-64767.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/64512-64767.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/64512-64767.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/64768-65023.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/64768-65023.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/64768-65023.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/64768-65023.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/65024-65279.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/65024-65279.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/65024-65279.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/65024-65279.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/65280-65535.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/65280-65535.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/65280-65535.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/65280-65535.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/6656-6911.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/6656-6911.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/6656-6911.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/6656-6911.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/6912-7167.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/6912-7167.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/6912-7167.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/6912-7167.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/7168-7423.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/7168-7423.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/7168-7423.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/7168-7423.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/7424-7679.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/7424-7679.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/7424-7679.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/7424-7679.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/768-1023.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/768-1023.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/768-1023.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/768-1023.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/7680-7935.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/7680-7935.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/7680-7935.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/7680-7935.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/7936-8191.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/7936-8191.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/7936-8191.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/7936-8191.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/8192-8447.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/8192-8447.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/8192-8447.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/8192-8447.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/8448-8703.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/8448-8703.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/8448-8703.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/8448-8703.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/8704-8959.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/8704-8959.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/8704-8959.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/8704-8959.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/8960-9215.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/8960-9215.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/8960-9215.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/8960-9215.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/9216-9471.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/9216-9471.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/9216-9471.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/9216-9471.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/9472-9727.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/9472-9727.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/9472-9727.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/9472-9727.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/9728-9983.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/9728-9983.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/9728-9983.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/9728-9983.pbf diff --git a/app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/9984-10239.pbf b/app/src/commonMain/composeResources/files/glyphs/Roboto Regular/9984-10239.pbf similarity index 100% rename from app/src/androidMain/assets/map_theme/glyphs/Roboto Regular/9984-10239.pbf rename to app/src/commonMain/composeResources/files/glyphs/Roboto Regular/9984-10239.pbf diff --git a/app/src/commonMain/composeResources/files/map-download-style.json b/app/src/commonMain/composeResources/files/map-download-style.json new file mode 100644 index 00000000000..b039fdf95bc --- /dev/null +++ b/app/src/commonMain/composeResources/files/map-download-style.json @@ -0,0 +1,13 @@ +{ + "version": 8, + "sources": { + "jawg": { + "type": "vector", + "tiles": [ + "https://tile.jawg.io/streets-v2+hillshade-v1/{z}/{x}/{y}.pbf?access-token=mL9X4SwxfsAGfojvGiion9hPKuGLKxPbogLyMbtakA2gJ3X88gcVlTSQ7OD6OfbZ" + ], + "maxzoom": 16 + } + }, + "layers": [] +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/CommonModule.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/CommonModule.kt index b7727e97985..7b31a5c22e8 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/CommonModule.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/CommonModule.kt @@ -172,6 +172,11 @@ import de.westnordost.streetcomplete.screens.main.MainViewModel import de.westnordost.streetcomplete.screens.main.MainViewModelImpl import de.westnordost.streetcomplete.screens.main.edithistory.EditHistoryViewModel import de.westnordost.streetcomplete.screens.main.edithistory.EditHistoryViewModelImpl +import de.westnordost.streetcomplete.screens.main.map.MainMapViewModel +import de.westnordost.streetcomplete.screens.main.map.MainMapViewModelImpl +import de.westnordost.streetcomplete.screens.main.map.sources.EditHistoryPinsSource +import de.westnordost.streetcomplete.screens.main.map.sources.MapQuestPinsSource +import de.westnordost.streetcomplete.screens.main.map.sources.StyleableOverlaySource import de.westnordost.streetcomplete.screens.settings.SettingsViewModel import de.westnordost.streetcomplete.screens.settings.SettingsViewModelImpl import de.westnordost.streetcomplete.screens.settings.debug.ShowQuestFormsViewModel @@ -600,8 +605,15 @@ val commonModule = module { EditHistoryViewModelImpl(get(), get()) } + viewModel { + MainMapViewModelImpl(get(), get(), get(), get(), get(), get()) + } + factory { MapQuestPinsSource(get(), get(), get()) } + factory { EditHistoryPinsSource(get()) } + factory { StyleableOverlaySource(get(), get()) } + viewModel { - MainBottomSheetViewModelImpl(get(), get(), get(), get(), get(), get(), get(), get()) + MainBottomSheetViewModelImpl(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(named("FeatureDictionaryLazy"))) } viewModel { ArMeasureViewModelImpl(get(), get()) } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/Cleaner.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/Cleaner.kt index 14b1e7775ff..0984a595179 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/Cleaner.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/Cleaner.kt @@ -42,6 +42,7 @@ class Cleaner( val deleted = mapDataController.deleteOlderThan(oldDataTimestamp, MAX_DELETE_ELEMENTS) if (deleted < MAX_DELETE_ELEMENTS) break } + mapTilesDownloader.deleteOld(oldDataTimestamp) downloadedTilesController.deleteOlderThan(oldDataTimestamp) // do this after cleaning map data and notes, because some metadata rely on map data questTypeRegistry.forEach { it.deleteMetadataOlderThan(oldDataTimestamp) } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/DatabaseImpl.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/DatabaseImpl.kt index 2eaefa2447c..ce075ae4086 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/DatabaseImpl.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/DatabaseImpl.kt @@ -142,7 +142,7 @@ class DatabaseImpl(private val connection: SQLiteConnection) : Database { } } - override fun close() { + override fun close() = lock.withLock { connection.close() } } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/location/Location.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/location/Location.kt index 561fedb75da..3d2ee6a299a 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/location/Location.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/location/Location.kt @@ -1,8 +1,10 @@ package de.westnordost.streetcomplete.data.location +import androidx.compose.runtime.Immutable import de.westnordost.streetcomplete.data.osm.mapdata.LatLon import kotlin.time.Duration +@Immutable data class Location( val position: LatLon, val accuracy: Float, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt index da75e70532b..0bf9361202f 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MapTilesDownloader.kt @@ -2,7 +2,15 @@ package de.westnordost.streetcomplete.data.maptiles import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox +/** Downloads map tiles for offline use */ interface MapTilesDownloader { + /** Fetch tiles in the given [bbox] */ suspend fun download(bbox: BoundingBox) + + /** Delete those tiles that are older than the given [time] */ + suspend fun deleteOld(time: Long) + + /** Delete all previously fetched tiles */ suspend fun clear() + } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MaplibreMapTilesDownloader.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MaplibreMapTilesDownloader.kt new file mode 100644 index 00000000000..45c419cfa96 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/maptiles/MaplibreMapTilesDownloader.kt @@ -0,0 +1,109 @@ +package de.westnordost.streetcomplete.data.maptiles + +import androidx.compose.runtime.snapshotFlow +import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.screens.main.map.toGeoJsonBoundingBox +import de.westnordost.streetcomplete.util.ktx.format +import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds +import de.westnordost.streetcomplete.util.logs.Log +import kotlinx.coroutines.flow.first +import org.maplibre.compose.offline.DownloadProgress +import org.maplibre.compose.offline.DownloadStatus +import org.maplibre.compose.offline.OfflineManager +import org.maplibre.compose.offline.OfflinePackDefinition +import kotlin.coroutines.cancellation.CancellationException + +class MapLibreMapTilesDownloader( + private val manager: OfflineManager, + private val pixelRatio: Float, +) : MapTilesDownloader { + + override suspend fun download(bbox: BoundingBox) { + val pack = manager.create( + definition = OfflinePackDefinition.TilePyramid( + // Only tiles need downloading; glyphs and images are packaged with the app. + styleUrl = Res.getUri("files/map-download-style.json"), + bounds = bbox.toGeoJsonBoundingBox(), + minZoom = 0, + maxZoom = 16, + pixelRatio = pixelRatio, + ), + // store timestamp as metadata for deleting areas older than X + metadata = nowAsEpochMilliseconds().toString().encodeToByteArray(), + ) + val startedAt = nowAsEpochMilliseconds() + try { + manager.resume(pack) + + // TODO maplibre-compose: Collect downloadProgress directly after upgrading to + // a release containing https://github.com/maplibre/maplibre-compose/pull/1405. + val finalState = snapshotFlow { pack.downloadProgress }.first { it.isFinished } + when (finalState) { + is DownloadProgress.Healthy -> { + val seconds = (nowAsEpochMilliseconds() - startedAt) / 1000.0 + Log.i( + TAG, + "Downloaded ${finalState.completedTileCount} tiles " + + "(${finalState.completedTileBytes / 1000}kB) in ${seconds.format(1)}s", + ) + } + is DownloadProgress.Error -> { + error("MapLibre offline download failed (${finalState.reason}): ${finalState.message}") + } + is DownloadProgress.TileLimitExceeded -> { + error("MapLibre offline tile limit ${finalState.limit} was exceeded") + } + DownloadProgress.Unknown -> { + error("Unexpected terminal offline progress") + } + } + } catch (error: Exception) { + try { + manager.pause(pack) + } catch (pauseError: Exception) { + error.addSuppressed(pauseError) + } + if (error !is CancellationException) { + Log.w(TAG, error.message.orEmpty(), error) + } + throw error + } + } + + override suspend fun deleteOld(time: Long) { + // TODO maplibre-compose: Read packs.value after upgrading to a release containing + // https://github.com/maplibre/maplibre-compose/pull/1405, which awaits initial loading. + val packs = manager.packs.toList() + for (pack in packs) { + val packTime = pack.metadata?.decodeToString()?.toLongOrNull() + if (packTime == null || packTime < time) { + manager.delete(pack) + } + } + } + + override suspend fun clear() { + try { + // TODO maplibre-compose: await initial pack loading here too (see deleteOld). + val packs = manager.packs.toList() + for (pack in packs) { manager.delete(pack) } + manager.clearAmbientCache() + } catch (error: CancellationException) { + throw error + } catch (error: Exception) { + Log.w(TAG, error.message.orEmpty(), error) + } + } + + private companion object { + private const val TAG = "MapTilesDownload" + } +} + +private val DownloadProgress.isFinished: Boolean get() = when (this) { + is DownloadProgress.Healthy -> status == DownloadStatus.Complete + is DownloadProgress.Error, + is DownloadProgress.TileLimitExceeded -> true + DownloadProgress.Unknown -> false +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/MapDataControllerImpl.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/MapDataControllerImpl.kt index 55403aa1e0b..5298e78b2fe 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/MapDataControllerImpl.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/MapDataControllerImpl.kt @@ -277,13 +277,13 @@ class MapDataControllerImpl constructor( companion object { private const val TAG = "MapDataController" - // StyleableOverlayManager loads z16 tiles, but we want smaller tiles. Small tiles make db + // StyleableOverlaySource loads z16 tiles, but we want smaller tiles. Small tiles make db // fetches for typical getMapDataWithGeometry calls noticeably faster than z16, as they // usually only require a small area. private const val SPATIAL_CACHE_TILE_ZOOM = 17 // Three times the maximum number of tiles that can be loaded at once in - // StyleableOverlayManager (translated from z16 tiles). We don't want to drop tiles from + // StyleableOverlaySource (translated from z16 tiles). We don't want to drop tiles from // cache already when scrolling the map just a bit, especially considering automatic trim // may temporarily reduce cache size to 2/3 of maximum. private const val SPATIAL_CACHE_TILES = 192 diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/overlays/OverlayStyle.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/overlays/OverlayStyle.kt index e27d147760a..30db9f71330 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/overlays/OverlayStyle.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/overlays/OverlayStyle.kt @@ -29,7 +29,8 @@ sealed interface OverlayStyle { data class Polygon( /** polygon area color */ val color: Color, - /** icon resource to show on the point */ + /** icon resource to show on the point. It shall be drawn with + * [WithHaloPainter][de.westnordost.streetcomplete.ui.util.WithHaloPainter] */ val icon: DrawableResource? = null, /** label to show in the center of the area */ val label: String? = null, @@ -40,7 +41,8 @@ sealed interface OverlayStyle { ) : OverlayStyle data class Point( - /** icon id to show on the point */ + /** icon resource to show on the point. It shall be drawn with + * [WithHaloPainter][de.westnordost.streetcomplete.ui.util.WithHaloPainter] */ val icon: DrawableResource?, /** label to show on the point */ val label: String? = null, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/quest/VisibleQuestsSource.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/quest/VisibleQuestsSource.kt index 1fbc2054807..c3d18dba34e 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/quest/VisibleQuestsSource.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/quest/VisibleQuestsSource.kt @@ -203,7 +203,7 @@ class VisibleQuestsSource( } } -// same tile zoom as used in QuestPinsManager which is the only caller of getAllVisible and only +// same tile zoom as used in MapQuestPinsSource which is the only caller of getAllVisible and only // ever queries tiles in that zoom private const val SPATIAL_CACHE_TILE_ZOOM = 16 // set a large number of tiles, as the cache is not large in memory and it allows diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainBottomSheetSelection.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainBottomSheetSelection.kt new file mode 100644 index 00000000000..600ffc247cb --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainBottomSheetSelection.kt @@ -0,0 +1,34 @@ +package de.westnordost.streetcomplete.screens.main + +import androidx.compose.runtime.saveable.Saver +import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.osmtracks.Trackpoint +import de.westnordost.streetcomplete.data.quest.QuestKey +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +sealed interface MainBottomSheetSelection { + @Serializable + data class Quest(val key: QuestKey) : MainBottomSheetSelection + + @Serializable + data class Overlay(val name: String, val elementKey: ElementKey? = null) : MainBottomSheetSelection + + @Serializable + data class CreateNote(val position: LatLon, val trackpoints: List? = null) : MainBottomSheetSelection + + companion object { + val Saver = Saver( + save = { selection -> + selection?.let { + // Bound instance state only; the live note still contains the full recording. + val saved = if (it is CreateNote) it.copy(trackpoints = it.trackpoints?.takeLast(1000)) else it + Json.encodeToString(saved) + } + }, + restore = { Json.decodeFromString(it) }, + ) + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainBottomSheetViewModel.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainBottomSheetViewModel.kt index 0edac77586d..70a3137d8e5 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainBottomSheetViewModel.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainBottomSheetViewModel.kt @@ -1,8 +1,8 @@ package de.westnordost.streetcomplete.screens.main import androidx.compose.runtime.Stable -import androidx.compose.ui.geometry.Offset import androidx.lifecycle.ViewModel +import de.westnordost.osmfeatures.FeatureDictionary import de.westnordost.streetcomplete.data.location.SurveyChecker import de.westnordost.streetcomplete.data.osm.edits.ElementEditAction import de.westnordost.streetcomplete.data.osm.edits.ElementEditType @@ -13,6 +13,9 @@ import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox import de.westnordost.streetcomplete.data.osm.mapdata.Element import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.osm.mapdata.LazyMapDataWithGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.key import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuestSource import de.westnordost.streetcomplete.data.osmnotes.Note import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditAction @@ -21,33 +24,37 @@ import de.westnordost.streetcomplete.data.osmnotes.edits.NotesWithEditsSource import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestSource import de.westnordost.streetcomplete.data.osmtracks.Trackpoint import de.westnordost.streetcomplete.data.overlays.Overlay +import de.westnordost.streetcomplete.data.overlays.OverlayRegistry import de.westnordost.streetcomplete.data.quest.OsmNoteQuestKey import de.westnordost.streetcomplete.data.quest.OsmQuestKey +import de.westnordost.streetcomplete.data.quest.Quest import de.westnordost.streetcomplete.data.quest.QuestKey +import de.westnordost.streetcomplete.data.quest.VisibleQuestsSource import de.westnordost.streetcomplete.data.visiblequests.QuestsHiddenController +import de.westnordost.streetcomplete.osm.level.levelsIntersect +import de.westnordost.streetcomplete.osm.level.parseLevelsOrNull +import de.westnordost.streetcomplete.screens.main.map.getIcon +import de.westnordost.streetcomplete.screens.main.map.getTitle +import de.westnordost.streetcomplete.ui.common.quest.Marker import de.westnordost.streetcomplete.util.ktx.launch import de.westnordost.streetcomplete.util.ktx.truncateTo6Decimals import de.westnordost.streetcomplete.util.math.enlargedBy import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.IO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.withContext @Stable abstract class MainBottomSheetViewModel : ViewModel() { - abstract val shownBottomSheet: StateFlow - - abstract val geometryOffsetInWindow: MutableStateFlow - - abstract fun showCreateElementInOverlay(overlay: Overlay) - - abstract fun showElementInOverlay(overlay: Overlay, elementKey: ElementKey) - - abstract fun showQuest(questKey: QuestKey) - - abstract fun showCreateNote(trackpoints: List?) - - abstract fun closeBottomSheet() + abstract fun bottomSheet(selection: MainBottomSheetSelection): Flow + abstract suspend fun getHighlightedMarkers(sheet: ShownBottomSheet): List abstract fun hideQuest(questKey: QuestKey) @@ -81,50 +88,108 @@ class MainBottomSheetViewModelImpl( private val noteEditsController: NoteEditsController, private val hiddenQuestsController: QuestsHiddenController, private val surveyChecker: SurveyChecker, + private val visibleQuestsSource: VisibleQuestsSource, + private val overlayRegistry: OverlayRegistry, + private val featureDictionary: Lazy, ) : MainBottomSheetViewModel() { - override val shownBottomSheet = MutableStateFlow(null) - - override val geometryOffsetInWindow = MutableStateFlow(null) - - override fun closeBottomSheet() { - shownBottomSheet.value = null - } - - override fun showCreateElementInOverlay(overlay: Overlay) { - shownBottomSheet.value = ShownBottomSheet.Overlay(overlay, null, null) - } - - override fun showElementInOverlay(overlay: Overlay, elementKey: ElementKey) { - launch(Dispatchers.IO) { - showElementInOverlayOrNote(overlay, elementKey) + @OptIn(ExperimentalCoroutinesApi::class) + override fun bottomSheet(selection: MainBottomSheetSelection): Flow { + if (selection is MainBottomSheetSelection.CreateNote) { + return flowOf(ShownBottomSheet.CreateOsmNote(selection.trackpoints)) + } + if (selection is MainBottomSheetSelection.Overlay && selection.elementKey == null) { + return flowOf(overlayRegistry.getByName(selection.name)?.let { ShownBottomSheet.Overlay(it, null, null) }) + } + return callbackFlow { + val elementKey = when (selection) { + is MainBottomSheetSelection.Overlay -> selection.elementKey + is MainBottomSheetSelection.Quest -> (selection.key as? OsmQuestKey)?.let { + ElementKey(it.elementType, it.elementId) + } + else -> null + } + val questListener = object : VisibleQuestsSource.Listener { + override fun onUpdated(added: Collection, removed: Collection) { + if (selection is MainBottomSheetSelection.Quest && + (selection.key in removed || added.any { it.key == selection.key })) trySend(Unit) + } + override fun onInvalidated() { trySend(Unit) } + } + val elementListener = object : MapDataWithEditsSource.Listener { + override fun onUpdated(updated: MapDataWithGeometry, deleted: Collection) { + if (elementKey != null && (elementKey in deleted || + updated.any { it.key == elementKey })) trySend(Unit) + } + override fun onReplacedForBBox(bbox: BoundingBox, mapDataWithGeometry: MapDataWithGeometry) { trySend(Unit) } + override fun onCleared() { trySend(Unit) } + } + visibleQuestsSource.addListener(questListener) + mapDataSource.addListener(elementListener) + trySend(Unit) + awaitClose { + visibleQuestsSource.removeListener(questListener) + mapDataSource.removeListener(elementListener) + } + }.buffer(Channel.CONFLATED).mapLatest { + withContext(Dispatchers.IO) { + when (selection) { + is MainBottomSheetSelection.Quest -> getQuest(selection.key) + is MainBottomSheetSelection.Overlay -> { + val overlay = overlayRegistry.getByName(selection.name) ?: return@withContext null + val key = selection.elementKey + if (key == null) ShownBottomSheet.Overlay(overlay, null, null) + else getElementInOverlay(overlay, key) + } + is MainBottomSheetSelection.CreateNote -> ShownBottomSheet.CreateOsmNote(selection.trackpoints) + } + } } } - private suspend fun showElementInOverlayOrNote(overlay: Overlay, elementKey: ElementKey) { - val geometry = mapDataSource.getGeometry(elementKey.type, elementKey.id) ?: return - - // a note at the position of the element blocks editing of that element + private fun getElementInOverlay(overlay: Overlay, key: ElementKey): ShownBottomSheet? { + val geometry = mapDataSource.getGeometry(key.type, key.id) ?: return null + // A note at the position of the element blocks editing that element. val note = getNoteForElementAt(geometry.center) - if (note != null) { - val quest = osmNoteQuestSource.get(note.id) ?: return - shownBottomSheet.value = ShownBottomSheet.OsmNoteQuest(quest, note) + return if (note != null) { + val quest = osmNoteQuestSource.get(note.id) ?: return null + ShownBottomSheet.OsmNoteQuest(quest, note) } else { - val element = mapDataSource.get(elementKey.type, elementKey.id) ?: return - shownBottomSheet.value = ShownBottomSheet.Overlay(overlay, element, geometry) + val element = mapDataSource.get(key.type, key.id) ?: return null + ShownBottomSheet.Overlay(overlay, element, geometry) } } - override fun showQuest(questKey: QuestKey) { - launch(Dispatchers.IO) { - when (questKey) { - is OsmNoteQuestKey -> showOsmNoteQuest(questKey) - is OsmQuestKey -> showOsmQuest(questKey) + private fun getQuest(key: QuestKey): ShownBottomSheet? { + if (visibleQuestsSource.get(key) == null) return null + return when (key) { + is OsmQuestKey -> { + val quest = osmQuestSource.get(key) ?: return null + val element = mapDataSource.get(key.elementType, key.elementId) ?: return null + ShownBottomSheet.OsmQuest(quest, element) + } + is OsmNoteQuestKey -> { + val quest = osmNoteQuestSource.get(key.noteId) ?: return null + val note = notesSource.get(key.noteId) ?: return null + ShownBottomSheet.OsmNoteQuest(quest, note) } } } - override fun showCreateNote(trackpoints: List?) { - shownBottomSheet.value = ShownBottomSheet.CreateOsmNote(trackpoints) + override suspend fun getHighlightedMarkers(sheet: ShownBottomSheet): List = withContext(Dispatchers.IO) { + if (sheet !is ShownBottomSheet.OsmQuest) return@withContext emptyList() + val quest = sheet.quest + val element = sheet.element + val bbox = quest.geometry.bounds.enlargedBy(quest.type.highlightedElementsRadius) + val mapData = LazyMapDataWithGeometry(bbox, mapDataSource) + val levels = parseLevelsOrNull(element.tags) + quest.type.getHighlightedElements(element, mapData).mapNotNull { other -> + // Highlight nearby elements only on the same level and layer. + if (element == other) return@mapNotNull null + if (!levels.levelsIntersect(parseLevelsOrNull(other.tags))) return@mapNotNull null + if (element.tags["layer"] != other.tags["layer"]) return@mapNotNull null + val geometry = mapData.getGeometry(other.type, other.id) ?: return@mapNotNull null + Marker(geometry, getIcon(featureDictionary.value, other), getTitle(other.tags)) + }.toList() } override fun hideQuest(questKey: QuestKey) { @@ -168,18 +233,6 @@ class MainBottomSheetViewModelImpl( } } - private fun showOsmQuest(questKey: OsmQuestKey) { - val element = mapDataSource.get(questKey.elementType, questKey.elementId) ?: return - val quest = osmQuestSource.get(questKey) ?: return - shownBottomSheet.value = ShownBottomSheet.OsmQuest(quest, element) - } - - private fun showOsmNoteQuest(questKey: OsmNoteQuestKey) { - val note = notesSource.get(questKey.noteId) ?: return - val quest = osmNoteQuestSource.get(questKey.noteId) ?: return - shownBottomSheet.value = ShownBottomSheet.OsmNoteQuest(quest, note) - } - private fun getNoteForElementAt(position: LatLon): Note? = notesSource .getAll(BoundingBox(position, position).enlargedBy(0.2)) @@ -196,6 +249,7 @@ sealed interface ShownBottomSheet { val element: Element, ) : ShownBottomSheet { override val position get() = quest.position + override val geometry get() = quest.geometry } data class OsmNoteQuest( @@ -203,12 +257,13 @@ sealed interface ShownBottomSheet { val note: Note ) : ShownBottomSheet { override val position get() = quest.position + override val geometry get() = quest.geometry } data class Overlay( val overlay: de.westnordost.streetcomplete.data.overlays.Overlay, val element: Element?, - val geometry: ElementGeometry?, + override val geometry: ElementGeometry?, ) : ShownBottomSheet { override val position get() = geometry?.center } @@ -217,7 +272,9 @@ sealed interface ShownBottomSheet { val trackpoints: List? ) : ShownBottomSheet { override val position get() = null + override val geometry get() = null } val position: LatLon? + val geometry: ElementGeometry? } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainScreen.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainScreen.kt index 465544aea8a..d33bda578e6 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainScreen.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainScreen.kt @@ -10,30 +10,68 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.AlertDialog +import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle import de.westnordost.streetcomplete.ApplicationConstants +import de.westnordost.streetcomplete.data.download.tiles.asBoundingBoxOfEnclosingTiles +import de.westnordost.streetcomplete.data.edithistory.EditKey +import de.westnordost.streetcomplete.data.location.Location import de.westnordost.streetcomplete.data.messages.Message +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.osmtracks.Trackpoint import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.screens.main.bottom_sheet.MainBottomSheet +import de.westnordost.streetcomplete.screens.main.controls.LocationState import de.westnordost.streetcomplete.screens.main.controls.MainScreenControls +import de.westnordost.streetcomplete.screens.main.controls.PointerPinButton import de.westnordost.streetcomplete.screens.main.edithistory.EditHistorySidebar import de.westnordost.streetcomplete.screens.main.edithistory.EditHistoryViewModel import de.westnordost.streetcomplete.screens.main.errors.LastCrashEffect import de.westnordost.streetcomplete.screens.main.errors.LastDownloadErrorEffect import de.westnordost.streetcomplete.screens.main.errors.LastUploadErrorEffect +import de.westnordost.streetcomplete.screens.main.map.BASE_STYLE +import de.westnordost.streetcomplete.screens.main.map.MainMap +import de.westnordost.streetcomplete.screens.main.map.MainMapContent +import de.westnordost.streetcomplete.screens.main.map.MainMapViewModel +import de.westnordost.streetcomplete.screens.main.map.getTrackBearing +import de.westnordost.streetcomplete.screens.main.map.rememberMainMapCameraState +import de.westnordost.streetcomplete.screens.main.map.rememberMainMapTrackState +import de.westnordost.streetcomplete.screens.main.map.toPosition +import de.westnordost.streetcomplete.screens.main.map.toStreetCompleteBoundingBox import de.westnordost.streetcomplete.screens.main.messages.MessageDialog import de.westnordost.streetcomplete.screens.main.teammode.TeamModeWizard import de.westnordost.streetcomplete.screens.main.urlconfig.ApplyUrlConfigEffect @@ -44,36 +82,66 @@ import de.westnordost.streetcomplete.ui.common.ToastPopup import de.westnordost.streetcomplete.ui.common.quest.MapClick import de.westnordost.streetcomplete.ui.common.quest.Marker import de.westnordost.streetcomplete.ui.ktx.dir +import de.westnordost.streetcomplete.ui.theme.Dimensions +import de.westnordost.streetcomplete.ui.util.rememberSerializable +import de.westnordost.streetcomplete.util.ktx.toLatLon +import de.westnordost.streetcomplete.util.ktx.toLocation +import de.westnordost.streetcomplete.util.ktx.updatesWithPermissionChanges +import de.westnordost.streetcomplete.util.math.area +import de.westnordost.streetcomplete.util.math.enclosingBoundingBox +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource - -/** Map controls shown on top of the map. */ +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel +import org.maplibre.compose.interaction.ClickResult +import org.maplibre.compose.location.HeadingMeasurement +import org.maplibre.compose.location.HeadingProvider +import org.maplibre.compose.location.HeadingRequest +import org.maplibre.compose.location.LocationEvent +import org.maplibre.compose.location.LocationMeasurement +import org.maplibre.compose.location.LocationPermission +import org.maplibre.compose.location.LocationProvider +import org.maplibre.compose.location.LocationRequest +import org.maplibre.compose.location.LocationUnavailableReason +import org.maplibre.compose.location.SystemSettingsLauncher +import org.maplibre.compose.map.LocalMapState +import org.maplibre.compose.map.MapRuntime +import org.maplibre.compose.map.rememberMapState +import org.maplibre.compose.overlay.attributions +import org.maplibre.compose.style.BaseStyle +import org.maplibre.spatialk.units.Bearing +import org.maplibre.spatialk.units.International +import org.maplibre.spatialk.units.extensions.degrees +import org.maplibre.spatialk.units.extensions.inDegrees +import kotlin.math.PI +import kotlin.math.sqrt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.uuid.Uuid + +/** The map and its controls, forms, and sidebars. */ @Composable fun MainScreen( - viewModel: MainViewModel, - editHistoryViewModel: EditHistoryViewModel, - mainBottomSheetViewModel: MainBottomSheetViewModel, - onClickZoomIn: () -> Unit, - onClickZoomOut: () -> Unit, - onZoomDrag: (Float) -> Unit, - onClickCompass: () -> Unit, - onClickLocation: () -> Unit, - onClickLocationPointer: () -> Unit, - onClickCreate: () -> Unit, - onClickStopTrackRecording: () -> Unit, - onDownload: () -> Unit, onClickSettings: () -> Unit, onClickQuestSettings: () -> Unit, onClickAbout: () -> Unit, onClickProfile: () -> Unit, onClickLogin: () -> Unit, - onSetMapMarkers: (Iterable) -> Unit, - onSolvedQuest: (icon: DrawableResource, position: LatLon) -> Unit, - getOffset: (position: LatLon) -> Offset?, - lastMapClick: MapClick?, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + uri: String? = null, + onConsumedUri: () -> Unit = {}, + viewModel: MainViewModel = koinViewModel(), + editHistoryViewModel: EditHistoryViewModel = koinViewModel(), + mainBottomSheetViewModel: MainBottomSheetViewModel = koinViewModel(), + mapViewModel: MainMapViewModel = koinViewModel(), + runtime: MapRuntime = koinInject(), + locationProvider: LocationProvider = koinInject(), + headingProvider: HeadingProvider = koinInject(), + systemSettingsLauncher: SystemSettingsLauncher = koinInject(), ) { val scope = rememberCoroutineScope() @@ -100,29 +168,10 @@ fun MainScreen( val lastDownloadError by viewModel.lastDownloadError.collectAsState() val lastUploadError by viewModel.lastUploadError.collectAsState() - val locationState by viewModel.locationState.collectAsState() - val isNavigationMode by viewModel.isNavigationMode.collectAsState() - val isFollowingPosition by viewModel.isFollowingPosition.collectAsState() - val isRecordingTracks by viewModel.isRecordingTracks.collectAsState() - val userHasMovedCamera by viewModel.userHasMovedCamera.collectAsState() - - val mapCamera by viewModel.mapCamera.collectAsState() - val metersPerDp by viewModel.metersPerDp.collectAsState() - val displayedPosition by viewModel.displayedPosition.collectAsState() - val showZoomButtons by viewModel.showZoomButtons.collectAsState() val isRequestingLogin by viewModel.isRequestingLogin.collectAsState() - val showEditHistorySidebar by editHistoryViewModel.isShowingSidebar.collectAsState() - - val editItems by editHistoryViewModel.editItems.collectAsState() - val selectedEdit by editHistoryViewModel.selectedEdit.collectAsState() - val hasEdits by remember { derivedStateOf { editItems.isNotEmpty() } } - - val shownBottomSheet by mainBottomSheetViewModel.shownBottomSheet.collectAsState() - val geometryOffsetInWindow by mainBottomSheetViewModel.geometryOffsetInWindow.collectAsState() - val emailAppLauncher = rememberEmailAppLauncher() var confirmReplaceDownload by remember { mutableStateOf(false) } @@ -133,12 +182,319 @@ fun MainScreen( var shownMessage by remember { mutableStateOf(null) } var showToast by remember { mutableStateOf(null) } + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + val windowInfo = LocalWindowInfo.current + val lifecycleOwner = LocalLifecycleOwner.current + val formStateHolder = rememberSaveableStateHolder() + var sheetSelection by rememberSaveable(stateSaver = MainBottomSheetSelection.Saver) { + mutableStateOf(null) + } + var sheetId by rememberSaveable { mutableStateOf("") } + var focusSheet by rememberSaveable { mutableStateOf(false) } + var showEditHistorySidebar by rememberSaveable { mutableStateOf(false) } + var selectedEditKey by rememberSerializable { mutableStateOf(null) } + var focusEdit by rememberSaveable { mutableStateOf(false) } + var displayedLocation by rememberSerializable { mutableStateOf(null) } + var location by remember { + mutableStateOf(displayedLocation?.let { + Location(it.position.toLatLon(), it.horizontalAccuracy?.toFloat(International.Meters) ?: 0f, Duration.ZERO) + }) + } + var heading by remember { mutableStateOf(null) } + var locationState by remember { mutableStateOf(LocationState.ENABLED) } + var userHasMovedCamera by rememberSaveable { mutableStateOf(false) } + var formMarkers by remember { mutableStateOf?>(null) } + var lastMapClick by remember { mutableStateOf(null) } + var lastLongPress by remember { mutableStateOf?>(null) } + var showMapContextMenu by remember { mutableStateOf(false) } + var lastQuestSolved by remember { mutableStateOf(null) } + var mapOrigin by remember { mutableStateOf(Offset.Zero) } + var locationDialog by remember { mutableStateOf(null) } + val tracks = rememberMainMapTrackState() + val editItems by editHistoryViewModel.editItems.collectAsState() + val selectedEdit = if (showEditHistorySidebar) editItems?.find { it.edit.key == selectedEditKey }?.edit else null + val hasEdits = !editItems.isNullOrEmpty() + val downloadedTiles by mapViewModel.downloadedTiles.collectAsState() + val geoUri by viewModel.geoUri.collectAsState() + val mapAppLauncher = rememberMapAppLauncher() + + fun showSheet(selection: MainBottomSheetSelection) { + formStateHolder.removeState(sheetId) + sheetId = Uuid.random().toString() + sheetSelection = selection + focusSheet = true + formMarkers = null + lastMapClick = null + } + + fun closeSheet() { + sheetSelection = null + formMarkers = null + lastMapClick = null + formStateHolder.removeState(sheetId) + } + + val sheetState = key(sheetSelection) { + produceState(null, mainBottomSheetViewModel) { + val selection = sheetSelection ?: return@produceState + mainBottomSheetViewModel.bottomSheet(selection).collect { sheet -> + if (selection is MainBottomSheetSelection.Overlay && sheet is ShownBottomSheet.OsmNoteQuest) { + // Save the blocking note's key so hiding or deleting it closes its form. + sheetSelection = MainBottomSheetSelection.Quest(sheet.quest.key) + return@collect + } + value = sheet + // A null emission means the selected object no longer exists, not loading. + if (sheet == null) closeSheet() + } + } + } + val shownBottomSheet by sheetState + val currentSelectedEdit by rememberUpdatedState(selectedEdit) + val highlightedGeometry by produceState(null, selectedEdit, editHistoryViewModel) { + value = selectedEdit?.let { editHistoryViewModel.getEditGeometry(it) } + } + val highlightedMarkers by key(shownBottomSheet) { + produceState>(emptyList(), mainBottomSheetViewModel) { + value = shownBottomSheet?.let { mainBottomSheetViewModel.getHighlightedMarkers(it) }.orEmpty() + } + } + val markers = formMarkers ?: highlightedMarkers + + val initialCamera = remember(viewModel) { viewModel.initialCamera } + val mapState = rememberMapState(runtime, BaseStyle.Json(BASE_STYLE), initialCameraPosition = initialCamera) { + val state = checkNotNull(LocalMapState.current) + val showPinsAtZoom by remember(state) { derivedStateOf { state.cameraPosition.zoom >= 13 } } + val showOverlayAtZoom by remember(state) { derivedStateOf { state.cameraPosition.zoom >= 14 } } + val showOverlay = selectedOverlay != null && shownBottomSheet !is ShownBottomSheet.OsmQuest && + shownBottomSheet !is ShownBottomSheet.OsmNoteQuest && !showEditHistorySidebar + val questPins = if (!showEditHistorySidebar && sheetSelection == null && showPinsAtZoom) { + mapViewModel.questPins.collectAsState().value + } else emptyList() + val historyPins = if (showEditHistorySidebar && showPinsAtZoom) { + mapViewModel.editHistoryPins.collectAsState().value + } else emptyList() + val styledElements = if (showOverlay && showOverlayAtZoom) { + mapViewModel.styleableElements.collectAsState().value + } else emptyList() + MainMapContent( + location = location, + rotation = heading?.let { (it.bearing - (Bearing.North + state.cameraPosition.bearing.degrees)).inDegrees.toFloat() }, + isRecording = tracks.isRecording, + trackpoints = tracks.currentTrack.map { it.position }, + oldTrackpointsLists = tracks.previousTracks.map { track -> track.map { it.position } }, + shownBottomSheet = shownBottomSheet, + shownMarkers = markers, + isShowingUndoHistorySidebar = showEditHistorySidebar, + selectedOverlay = selectedOverlay, + selectedEdit = selectedEdit, + highlightedGeometry = highlightedGeometry, + downloadedTiles = downloadedTiles, + questPins = questPins, + editHistoryPins = historyPins, + styledElements = styledElements, + onClickQuest = { properties -> + val key = mapViewModel.getQuestKey(properties) + if (key == null || sheetSelection != null) ClickResult.Pass else { + showSheet(MainBottomSheetSelection.Quest(key)) + ClickResult.Consume + } + }, + onClickElement = { properties -> + val key = mapViewModel.getElementKey(properties) + val overlay = selectedOverlay + if (key == null || overlay == null || sheetSelection != null) ClickResult.Pass else { + showSheet(MainBottomSheetSelection.Overlay(overlay.name, key)) + ClickResult.Consume + } + }, + onClickEdit = { properties -> + val key = mapViewModel.getEditKey(properties) + if (key == null) ClickResult.Pass else { + selectedEditKey = key + focusEdit = true + ClickResult.Consume + } + }, + ) + } + val cameraState = rememberMainMapCameraState(mapState, viewModel.initiallyFollowing, viewModel.initiallyNavigating) + val mapCamera = mapState.cameraPosition + val viewport = mapState.viewport + val metersPerDp = remember(viewport, mapCamera) { + mapState.metersPerDpAtLatitude(mapCamera.target.latitude) ?: 0.0 + } + val sheetPadding = Dimensions.getOpenQuestFormMapPadding(windowInfo) + val cameraPadding = if (sheetSelection != null) sheetPadding else PaddingValues(0.dp) + val isNavigationMode = cameraState.isNavigationMode + val isFollowingPosition = cameraState.isFollowingPosition + val isRecordingTracks = tracks.isRecording + + fun getOffset(position: LatLon): Offset? = mapState.screenLocationFromPosition(position.toPosition())?.let { + with(density) { Offset(it.x.toPx(), it.y.toPx()) } + mapOrigin + } + val geometryOffsetInWindow = remember(shownBottomSheet, viewport, mapCamera, mapOrigin) { + shownBottomSheet?.position?.let(::getOffset) + } + fun followLocation() { + cameraState.isFollowingPosition = true + scope.launch { cameraState.followLocation(displayedLocation?.position?.toLatLon(), getTrackBearing(tracks.currentTrack)) } + } + fun zoomBy(amount: Double) { + scope.launch { mapState.animateCameraPosition(mapState.cameraPosition.copy(zoom = mapState.cameraPosition.zoom + amount), 300.milliseconds) } + } + fun composeNote(position: LatLon, trackpoints: List? = null) { + showSheet(MainBottomSheetSelection.CreateNote(position, trackpoints)) + } + fun download() { + val bounds = mapState.viewport?.visibleBounds?.toStreetCompleteBoundingBox() + if (bounds == null) { + showToast = Toast.CannotFindBounds + return + } + val tilesBounds = bounds.asBoundingBoxOfEnclosingTiles(ApplicationConstants.DOWNLOAD_TILE_ZOOM) + val area = tilesBounds.area() / 1_000_000 + if (area > ApplicationConstants.MAX_DOWNLOADABLE_AREA_IN_SQKM) { + showToast = Toast.DownloadAreaTooBig + return + } + val downloadBounds = if (area < ApplicationConstants.MIN_DOWNLOADABLE_AREA_IN_SQKM) { + val radius = sqrt(1_000_000 * ApplicationConstants.MIN_DOWNLOADABLE_AREA_IN_SQKM / PI) + mapState.cameraPosition.target.toLatLon().enclosingBoundingBox(radius) + } else tilesBounds + viewModel.download(downloadBounds) + } + fun clickLocation() { + val permission = locationProvider.permission.value + if (permission is LocationPermission.NotGranted) { + when { + permission.canRequest == false -> { + if (systemSettingsLauncher.canOpenApplicationSettings) locationDialog = LocationDialog.ApplicationSettings + else showToast = Toast.NoLocation + } + permission.shouldShowRationale -> locationDialog = LocationDialog.PermissionRationale + else -> locationProvider.requestPermission() + } + } else when { + locationState == LocationState.ALLOWED -> { + if (systemSettingsLauncher.canOpenLocationServicesSettings) locationDialog = LocationDialog.LocationSettings + else showToast = Toast.NoLocation + } + !cameraState.isFollowingPosition -> followLocation() + else -> scope.launch { + cameraState.setNavigationMode(!cameraState.isNavigationMode, + displayedLocation?.position?.toLatLon(), getTrackBearing(tracks.currentTrack)) + } + } + } + + LaunchedEffect(uri) { + if (uri != null) { + viewModel.setUri(uri) + onConsumedUri() + } + } + LaunchedEffect(geoUri) { + geoUri?.let { + cameraState.moveTo(it) + viewModel.consumeGeoUri() + } + } + // Apply the MapLibre viewport to StreetComplete's quest and overlay data sources. + LaunchedEffect(mapState, mapViewModel) { + snapshotFlow { mapState.cameraPosition.zoom to mapState.viewport?.visibleBounds } + .collect { (zoom, bounds) -> mapViewModel.onViewportChanged(zoom, bounds?.toStreetCompleteBoundingBox()) } + } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { + viewModel.saveCamera(mapState.cameraPosition, cameraState.isFollowingPosition, cameraState.isNavigationMode) + } + LaunchedEffect(locationProvider, lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + locationProvider.updatesWithPermissionChanges(LocationRequest()).collect { event -> + when (event) { + is LocationEvent.Update -> { + val fix = event.toLocation() + // Survey checking receives every fix, including ones too inaccurate for a track. + mapViewModel.onLocationChanged(fix) + displayedLocation = event.measurement + location = fix + locationState = LocationState.UPDATING + tracks.addLocation(event.measurement) + launch { cameraState.followLocation(fix.position, getTrackBearing(tracks.currentTrack)) } + } + is LocationEvent.Unavailable -> { + locationState = when (event.reason) { + LocationUnavailableReason.ServicesDisabled -> LocationState.ALLOWED + LocationUnavailableReason.TemporarilyUnavailable -> LocationState.SEARCHING + LocationUnavailableReason.PermissionDenied -> LocationState.DENIED + LocationUnavailableReason.Unsupported, LocationUnavailableReason.UnexpectedFailure -> null + } + displayedLocation = null + location = null + tracks.clear() + launch { cameraState.setNavigationMode(false, null, null) } + } + } + } + } + } + LaunchedEffect(headingProvider, lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + headingProvider.updates(HeadingRequest(33.milliseconds)).collect { heading = it } + } + } + // Freeze follow/navigation before moving the camera for a form or history selection. + LaunchedEffect(mapState, sheetSelection, showEditHistorySidebar, selectedEditKey) { + val selection = sheetSelection + if (selection != null) { + cameraState.freeze() + if (focusSheet) { + val sheet = snapshotFlow { sheetState.value }.filterNotNull().first() + focusSheet = false + when (selection) { + is MainBottomSheetSelection.CreateNote -> mapState.animateCameraPosition( + mapState.cameraPosition.copy(target = selection.position.toPosition()), 300.milliseconds, + ) + else -> when (sheet) { + is ShownBottomSheet.OsmQuest -> cameraState.focus(sheet.quest.geometry) + is ShownBottomSheet.OsmNoteQuest -> cameraState.focus(sheet.quest.geometry) + else -> Unit + } + } + } + } else if (showEditHistorySidebar) { + cameraState.freeze() + if (selectedEditKey == null) cameraState.clearFocus() + else if (focusEdit) { + val edit = snapshotFlow { currentSelectedEdit }.filterNotNull().first() + focusEdit = false + cameraState.focus(editHistoryViewModel.getEditGeometry(edit)) + } + } else { + cameraState.unfreeze(displayedLocation?.position?.toLatLon(), getTrackBearing(tracks.currentTrack)) + cameraState.endFocus() + } + } + LaunchedEffect(editItems) { + val items = editItems ?: return@LaunchedEffect + if (items.none { it.edit.key == selectedEditKey }) selectedEditKey = null + if (items.isEmpty() && showEditHistorySidebar) { + cameraState.clearFocus() + showEditHistorySidebar = false + } + } + LaunchedEffect(selectedOverlay) { + val selection = sheetSelection as? MainBottomSheetSelection.Overlay + if (selection != null && selection.name != selectedOverlay?.name) closeSheet() + } + fun onClickDownload() { if (viewModel.isConnected) { if (viewModel.isUserInitiatedDownloadInProgress) { confirmReplaceDownload = true } else { - onDownload() + download() } } else { showToast = Toast.Offline @@ -191,6 +547,43 @@ fun MainScreen( } Box(modifier) { + MainMap( + state = mapState, + modifier = Modifier.fillMaxSize().onGloballyPositioned { mapOrigin = it.positionInWindow() }, + cameraPadding = cameraPadding, + onPan = { cameraState.onPan(displayedLocation != null) }, + onUserCameraMove = { userHasMovedCamera = true }, + onMapClick = { event -> + val position = event.position?.toLatLon() + if (sheetSelection != null && position != null) { + // Name suggestions use a small tap tolerance; rendered feature queries remain point-only. + lastMapClick = MapClick(position, metersPerDp * 14) + } else if (showEditHistorySidebar) { + cameraState.clearFocus() + showEditHistorySidebar = false + selectedEditKey = null + } + ClickResult.Consume + }, + onMapLongClick = { event -> + val position = event.position?.toLatLon() + if (sheetSelection == null && !showEditHistorySidebar && position != null) { + lastLongPress = event.screenOffset to position + showMapContextMenu = true + } + ClickResult.Consume + }, + overlay = { + if (!showIntroTutorial) { + displayedLocation?.position?.let { position -> + PointerPinButton(targetPosition = position, onClick = ::followLocation) { + Image(painterResource(Res.drawable.location_dot_small), null) + } + } + } + }, + ) + // TODO: Alternative to this would be to put the tutorial screens into a separate // navigation destination in a TBD MainNavHost after complete migration to Compose // (see #6255) @@ -207,6 +600,7 @@ fun MainScreen( overlays = overlays, selectedOverlay = selectedOverlay, onSelectOverlay = { overlay -> + if (sheetSelection is MainBottomSheetSelection.Overlay) closeSheet() viewModel.selectOverlay(overlay) if (!viewModel.hasShownOverlaysTutorial) { showOverlaysTutorial = true @@ -218,28 +612,41 @@ fun MainScreen( onClickMainMenu = { showMainMenuDialog = true }, showZoomButtons = showZoomButtons, - onClickZoomIn = onClickZoomIn, - onClickZoomOut = onClickZoomOut, - onZoomDrag = onZoomDrag, + onClickZoomIn = { zoomBy(1.0) }, + onClickZoomOut = { zoomBy(-1.0) }, + onZoomDrag = { zoomBy(it / 20.0) }, - mapRotation = mapCamera?.rotation?.toFloat() ?: 0f, - mapTilt = mapCamera?.tilt?.toFloat() ?: 0f, - onClickCompass = onClickCompass, + mapRotation = mapCamera.bearing.toFloat(), + mapTilt = mapCamera.tilt.toFloat(), + onClickCompass = { scope.launch { cameraState.resetCompass() } }, locationState = locationState, isNavigationMode = isNavigationMode, isFollowingPosition = isFollowingPosition, - displayedLocationOffset = displayedPosition, - onClickLocation = onClickLocation, - onClickLocationPointer = onClickLocationPointer, + onClickLocation = ::clickLocation, isRecordingTracks = isRecordingTracks, - onClickStopTrackRecording = onClickStopTrackRecording, + onClickStopTrackRecording = { + tracks.stopRecording() + displayedLocation?.position?.toLatLon()?.let { composeNote(it, tracks.recordedTrack.takeIf { it.isNotEmpty() }) } + }, isCreateNodeEnabled = isCreateNodeEnabled, onClickCreate = { - if ((mapCamera?.zoom ?: 0.0) >= 17.0) { - onClickCreate() + if (mapCamera.zoom >= 17.0) { + selectedOverlay?.let { overlay -> + val size = windowInfo.containerDpSize + val left = sheetPadding.calculateLeftPadding(layoutDirection) + val right = sheetPadding.calculateRightPadding(layoutDirection) + val top = sheetPadding.calculateTopPadding() + val bottom = sheetPadding.calculateBottomPadding() + val position = mapState.positionFromScreenLocation(DpOffset( + left + (size.width - left - right) / 2, + top + (size.height - top - bottom) / 2, + )) + showSheet(MainBottomSheetSelection.Overlay(overlay.name)) + position?.let { mapState.setCameraPosition(mapState.cameraPosition.copy(target = it)) } + } } else { showToast = Toast.DownloadAreaTooBig } @@ -247,56 +654,76 @@ fun MainScreen( hasEdits = hasEdits, isUndoEnabled = !isUploadingOrDownloading, - onClickUndo = { editHistoryViewModel.showSidebar() }, + onClickUndo = { + selectedEditKey = editItems?.lastOrNull()?.edit?.key + focusEdit = true + showEditHistorySidebar = true + }, metersPerDp = metersPerDp, + attributions = mapState.style.attributions(), userHasMovedMap = userHasMovedCamera, ) } val dir = LocalLayoutDirection.current.dir AnimatedVisibility( - visible = showEditHistorySidebar, + visible = showEditHistorySidebar && hasEdits, enter = fadeIn() + slideInHorizontally(initialOffsetX = { -it * dir }), exit = fadeOut() + slideOutHorizontally(targetOffsetX = { -it * dir }), ) { EditHistorySidebar( - editItems = editItems, + editItems = editItems.orEmpty(), selectedEdit = selectedEdit, - onSelectEdit = { editHistoryViewModel.select(it.key) }, + onSelectEdit = { + selectedEditKey = it.key + focusEdit = true + }, onUndoEdit = { editHistoryViewModel.undo(it.key) }, - onDismissRequest = { editHistoryViewModel.hideSidebar() }, + onDismissRequest = { + cameraState.clearFocus() + showEditHistorySidebar = false + selectedEditKey = null + }, getEditElement = editHistoryViewModel::getEditElement, ) } - mapCamera?.let { mapCamera -> - AnimatedContent( - targetState = shownBottomSheet, - transitionSpec = { - if (initialState != null && targetState != null) { - fadeIn() + slideInVertically { it / 16 } togetherWith fadeOut() - } else { - // Size transform with snap is necessary so that it doesn't animate the bounds - // from zero (=no form) which looks weird - (fadeIn() + slideInVertically { it } togetherWith - fadeOut() + slideOutVertically { it / 2 }) using SizeTransform(clip = false) - } - }, - ) { shownBottomSheet -> - if (shownBottomSheet != null) { + AnimatedContent( + targetState = shownBottomSheet?.let { sheetId to it }, + contentKey = { it?.first }, + transitionSpec = { + if (initialState != null && targetState != null) { + fadeIn() + slideInVertically { it / 16 } togetherWith fadeOut() + } else { + // Size transform with snap is necessary so that it doesn't animate the bounds + // from zero (=no form) which looks weird + (fadeIn() + slideInVertically { it } togetherWith + fadeOut() + slideOutVertically { it / 2 }) using SizeTransform(clip = false) + } + }, + ) { content -> + if (content != null) { + val (id, shownBottomSheet) = content + formStateHolder.SaveableStateProvider(id) { MainBottomSheet( - onDismiss = { mainBottomSheetViewModel.closeBottomSheet() }, - onSolved = onSolvedQuest, - viewModel = mainBottomSheetViewModel, + onDismiss = ::closeSheet, + onSolved = { icon, position -> + getOffset(position)?.let { lastQuestSolved = QuestSolvedEvent(icon, it) } + }, + onHideQuest = mainBottomSheetViewModel::hideQuest, + isSurvey = mainBottomSheetViewModel::isSurvey, + onSubmitEdit = mainBottomSheetViewModel::submitEdit, + onCommentNote = mainBottomSheetViewModel::commentNote, + onCreateNote = mainBottomSheetViewModel::createNote, shownBottomSheet = shownBottomSheet, geometryOffsetInWindow = geometryOffsetInWindow, - mapRotation = mapCamera.rotation.toFloat(), + mapRotation = mapCamera.bearing.toFloat(), mapTilt = mapCamera.tilt.toFloat(), - mapPosition = mapCamera.position, + mapPosition = mapCamera.target.toLatLon(), mapMetersPerDp = metersPerDp, - onSetMapMarkers = onSetMapMarkers, - getOffset = getOffset, + onSetMapMarkers = { if (id == sheetId) formMarkers = it.toList() }, + getOffset = ::getOffset, lastMapClick = lastMapClick, ) } @@ -304,6 +731,43 @@ fun MainScreen( } } + lastQuestSolved?.let { LastQuestSolvedEffect(it) } + MapContextMenu( + expanded = showMapContextMenu, + onDismissRequest = { showMapContextMenu = false }, + onClickCreateNote = { + if (mapState.cameraPosition.zoom < ApplicationConstants.NOTE_MIN_ZOOM) showToast = Toast.ImpreciseNote + else lastLongPress?.second?.let { composeNote(it) } + }, + onClickCreateTrack = { tracks.startRecording() }, + isOpenLocationAvailable = mapAppLauncher.isAvailable(), + onClickOpenLocation = { + lastLongPress?.second?.let { mapAppLauncher.openAt(it, mapState.cameraPosition.zoom) } + }, + offset = lastLongPress?.first ?: DpOffset.Zero, + ) + locationDialog?.let { dialog -> + AlertDialog( + onDismissRequest = { locationDialog = null }, + title = if (dialog == LocationDialog.PermissionRationale) { + { Text(stringResource(Res.string.no_location_permission_warning_title)) } + } else null, + text = { Text(stringResource(if (dialog == LocationDialog.PermissionRationale) + Res.string.no_location_permission_warning else Res.string.turn_on_location_request)) }, + confirmButton = { + TextButton(onClick = { + locationDialog = null + when (dialog) { + LocationDialog.PermissionRationale -> locationProvider.requestPermission() + LocationDialog.ApplicationSettings -> systemSettingsLauncher.openApplicationSettings() + LocationDialog.LocationSettings -> systemSettingsLauncher.openLocationServicesSettings() + } + }) { Text(stringResource(Res.string.ok)) } + }, + dismissButton = { TextButton(onClick = { locationDialog = null }) { Text(stringResource(Res.string.cancel)) } }, + ) + } + shownMessage?.let { message -> val questIcons = remember { viewModel.allQuestTypes.map { it.icon } } MessageDialog( @@ -361,7 +825,7 @@ fun MainScreen( if (confirmReplaceDownload) { ConfirmReplaceDownloadDialog( onDismissRequest = { confirmReplaceDownload = false }, - onConfirmed = { onDownload() } + onConfirmed = { download() } ) } @@ -406,13 +870,21 @@ private enum class Toast { TeamModeActive, TeamModeDeactivated, DownloadAreaTooBig, - NoEmailClient + NoEmailClient, + CannotFindBounds, + NoLocation, + ImpreciseNote } private val Toast.messageResource: StringResource get() = when (this) { + Toast.CannotFindBounds -> Res.string.cannot_find_bbox_or_reduce_tilt + Toast.NoLocation -> Res.string.no_gps_no_quests + Toast.ImpreciseNote -> Res.string.create_new_note_unprecise Toast.Offline -> Res.string.offline Toast.TeamModeActive -> Res.string.team_mode_active Toast.TeamModeDeactivated -> Res.string.team_mode_deactivated Toast.DownloadAreaTooBig -> Res.string.download_area_too_big Toast.NoEmailClient -> Res.string.no_email_client } + +private enum class LocationDialog { PermissionRationale, ApplicationSettings, LocationSettings } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainViewModel.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainViewModel.kt index 2d04be61ffc..722cf48673d 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainViewModel.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainViewModel.kt @@ -1,16 +1,13 @@ package de.westnordost.streetcomplete.screens.main -import androidx.compose.ui.geometry.Offset import androidx.lifecycle.ViewModel import de.westnordost.streetcomplete.data.messages.Message import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox import de.westnordost.streetcomplete.data.overlays.Overlay import de.westnordost.streetcomplete.data.quest.QuestType import de.westnordost.streetcomplete.data.urlconfig.UrlConfig -import de.westnordost.streetcomplete.screens.main.controls.LocationState -import de.westnordost.streetcomplete.screens.main.map.maplibre.CameraPosition -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import org.maplibre.compose.camera.CameraPosition import kotlin.reflect.KClass // not @Stable, as not all fields are StateFlows or immutable @@ -29,7 +26,12 @@ abstract class MainViewModel : ViewModel() { abstract val urlConfig: StateFlow abstract fun applyUrlConfig(config: UrlConfig) abstract val geoUri: StateFlow - abstract fun consumeGeoUri() // probably only necessary while not fully converted to Compose yet + abstract fun consumeGeoUri() + + abstract val initialCamera: CameraPosition + abstract val initiallyFollowing: Boolean + abstract val initiallyNavigating: Boolean + abstract fun saveCamera(camera: CameraPosition, following: Boolean, navigating: Boolean) /* intro */ abstract var hasShownTutorial: Boolean @@ -79,20 +81,6 @@ abstract class MainViewModel : ViewModel() { abstract val starsCount: StateFlow abstract val isShowingStarsCurrentWeek: StateFlow abstract fun toggleShowingCurrentWeek() - - // NOTE: currently filled from MainActivity (communication to compose view), i.e. the source of - // truth is actually the MapFragment - abstract val locationState: MutableStateFlow - abstract val mapCamera: MutableStateFlow - abstract val metersPerDp: MutableStateFlow - abstract val displayedPosition: MutableStateFlow - - abstract val isFollowingPosition: MutableStateFlow - abstract val isNavigationMode: MutableStateFlow - - abstract val isRecordingTracks: MutableStateFlow - - abstract val userHasMovedCamera: MutableStateFlow } data class ShownUrlConfig(val urlConfig: UrlConfig, val alreadyExists: Boolean) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainViewModelImpl.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainViewModelImpl.kt index 5129aaa4325..890dede949e 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainViewModelImpl.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/MainViewModelImpl.kt @@ -1,6 +1,5 @@ package de.westnordost.streetcomplete.screens.main -import androidx.compose.ui.geometry.Offset import androidx.lifecycle.viewModelScope import de.westnordost.streetcomplete.data.UnsyncedChangesCountSource import de.westnordost.streetcomplete.data.connection.ActiveNetworkConnection @@ -33,11 +32,11 @@ import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource import de.westnordost.streetcomplete.data.visiblequests.TeamModeQuestFilterController import de.westnordost.streetcomplete.data.visiblequests.TeamModeQuestFilterSource import de.westnordost.streetcomplete.data.visiblequests.VisibleEditTypeSource -import de.westnordost.streetcomplete.screens.main.controls.LocationState -import de.westnordost.streetcomplete.screens.main.map.maplibre.CameraPosition +import de.westnordost.streetcomplete.screens.main.map.toPosition import de.westnordost.streetcomplete.util.error_reporting.CrashReportHolder import de.westnordost.streetcomplete.util.error_reporting.ErrorReportBuilder import de.westnordost.streetcomplete.util.ktx.launch +import de.westnordost.streetcomplete.util.ktx.toLatLon import de.westnordost.streetcomplete.util.parseGeoUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -53,6 +52,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.plus import kotlinx.coroutines.withContext +import org.maplibre.compose.camera.CameraPosition import kotlin.reflect.KClass class MainViewModelImpl( @@ -79,6 +79,22 @@ class MainViewModelImpl( private val prefs: Preferences, ) : MainViewModel() { + override val initialCamera get() = CameraPosition( + target = prefs.mapPosition.toPosition(), bearing = prefs.mapRotation, + tilt = prefs.mapTilt, zoom = prefs.mapZoom, + ) + override val initiallyFollowing get() = prefs.mapIsFollowing + override val initiallyNavigating get() = prefs.mapIsNavigationMode + + override fun saveCamera(camera: CameraPosition, following: Boolean, navigating: Boolean) { + prefs.mapPosition = camera.target.toLatLon() + prefs.mapRotation = camera.bearing + prefs.mapTilt = camera.tilt + prefs.mapZoom = camera.zoom + prefs.mapIsFollowing = following + prefs.mapIsNavigationMode = navigating + } + /* error handling */ override val lastCrashReport = MutableStateFlow(null) @@ -113,7 +129,7 @@ class MainViewModelImpl( val zoom = if (geo.zoom == null || geo.zoom < 14) 18.0 else geo.zoom val pos = LatLon(geo.latitude, geo.longitude) - geoUri.value = CameraPosition(pos, 0.0, 0.0, zoom) + geoUri.value = CameraPosition(target = pos.toPosition(), bearing = 0.0, tilt = 0.0, zoom = zoom) } } } @@ -206,7 +222,7 @@ class MainViewModelImpl( } selectedOverlayController.addListener(listener) awaitClose { selectedOverlayController.removeListener(listener) } - }.stateIn(viewModelScope + Dispatchers.IO, SharingStarted.Eagerly, null) + }.stateIn(viewModelScope + Dispatchers.IO, SharingStarted.Eagerly, selectedOverlayController.selectedOverlay) override var hasShownOverlaysTutorial: Boolean get() = prefs.hasShownOverlaysTutorial @@ -415,18 +431,6 @@ class MainViewModelImpl( syncedEdits + unsyncedEdits }.stateIn(viewModelScope + Dispatchers.IO, SharingStarted.Eagerly, 0) - override val locationState: MutableStateFlow = MutableStateFlow(LocationState.ENABLED) - override val mapCamera = MutableStateFlow(null) - override val metersPerDp = MutableStateFlow(0.0) - override val displayedPosition = MutableStateFlow(null) - - override val isFollowingPosition = MutableStateFlow(false) - override val isNavigationMode = MutableStateFlow(false) - - override val isRecordingTracks = MutableStateFlow(false) - - override val userHasMovedCamera = MutableStateFlow(false) - // --------------------------------------------------------------------------------------- init { diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/MainBottomSheet.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/MainBottomSheet.kt index 678c3e506dc..94bf6d7adb6 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/MainBottomSheet.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/MainBottomSheet.kt @@ -12,12 +12,14 @@ import de.westnordost.streetcomplete.data.osm.edits.ElementEditType import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.osmnotes.Note +import de.westnordost.streetcomplete.data.osmtracks.Trackpoint import de.westnordost.streetcomplete.data.quest.OsmNoteQuestKey import de.westnordost.streetcomplete.data.quest.OsmQuestKey +import de.westnordost.streetcomplete.data.quest.QuestKey import de.westnordost.streetcomplete.quests.note_comments.AddNoteCommentForm import de.westnordost.streetcomplete.resources.Res import de.westnordost.streetcomplete.resources.quest_create_note -import de.westnordost.streetcomplete.screens.main.MainBottomSheetViewModel import de.westnordost.streetcomplete.screens.main.ShownBottomSheet import de.westnordost.streetcomplete.screens.main.bottom_sheet.note.CreateNoteForm import de.westnordost.streetcomplete.screens.main.bottom_sheet.overlay.OverlayFormContainer @@ -38,7 +40,11 @@ import org.jetbrains.compose.resources.DrawableResource fun MainBottomSheet( onDismiss: () -> Unit, onSolved: (icon: DrawableResource, position: LatLon) -> Unit, - viewModel: MainBottomSheetViewModel, + onHideQuest: (QuestKey) -> Unit, + isSurvey: (ElementGeometry) -> Boolean, + onSubmitEdit: (ElementEditType, ElementGeometry, ElementEditAction) -> Unit, + onCommentNote: (Note, String?, List) -> Unit, + onCreateNote: (LatLon, String, List, List?) -> Unit, shownBottomSheet: ShownBottomSheet, geometryOffsetInWindow: Offset?, mapRotation: Float, @@ -56,11 +62,11 @@ fun MainBottomSheet( is ShownBottomSheet.CreateOsmNote -> { CreateNoteForm( onLeaveNote = { noteText, noteImagePaths, trackpoints -> - viewModel.createNote( - position = mapPosition, - text = noteText, - imagePaths = noteImagePaths, - trackpoints = trackpoints + onCreateNote( + mapPosition, + noteText, + noteImagePaths, + trackpoints ) onSolved(Res.drawable.quest_create_note, mapPosition) onDismiss() @@ -74,17 +80,17 @@ fun MainBottomSheet( AddNoteCommentForm( onDismiss = onDismiss, onCommentNote = { noteText, noteImagePaths -> - viewModel.commentNote( - note = shownBottomSheet.note, - text = noteText, - imagePaths = noteImagePaths + onCommentNote( + shownBottomSheet.note, + noteText, + noteImagePaths ) onSolved(shownBottomSheet.quest.type.icon, shownBottomSheet.quest.position) onDismiss() }, onHideQuest = { val key = OsmNoteQuestKey(shownBottomSheet.note.id) - viewModel.hideQuest(key) + onHideQuest(key) onDismiss() }, quest = shownBottomSheet.quest, @@ -96,11 +102,11 @@ fun MainBottomSheet( OsmQuestFormContainer( onDismiss = onDismiss, onEdit = { action -> - if (SuppressSurveyConfirmation || viewModel.isSurvey(shownBottomSheet.quest.geometry)) { - viewModel.submitEdit( - elementEditType = shownBottomSheet.quest.type, - geometry = shownBottomSheet.quest.geometry, - elementEditAction = action + if (SuppressSurveyConfirmation || isSurvey(shownBottomSheet.quest.geometry)) { + onSubmitEdit( + shownBottomSheet.quest.type, + shownBottomSheet.quest.geometry, + action ) onSolved(shownBottomSheet.quest.type.icon, shownBottomSheet.quest.position) onDismiss() @@ -109,10 +115,11 @@ fun MainBottomSheet( } }, onLeaveNote = { noteText, noteImagePaths -> - viewModel.createNote( - position = shownBottomSheet.quest.geometry.center, - text = noteText, - imagePaths = noteImagePaths, + onCreateNote( + shownBottomSheet.quest.geometry.center, + noteText, + noteImagePaths, + null, ) onSolved(shownBottomSheet.quest.type.icon, shownBottomSheet.quest.position) onDismiss() @@ -121,7 +128,7 @@ fun MainBottomSheet( val key = OsmQuestKey( shownBottomSheet.element.type, shownBottomSheet.element.id, shownBottomSheet.quest.type.name) - viewModel.hideQuest(key) + onHideQuest(key) onDismiss() }, questType = shownBottomSheet.quest.type, @@ -144,11 +151,11 @@ fun MainBottomSheet( onEdit = { action -> val geometry = shownBottomSheet.geometry ?: ElementPointGeometry(mapPosition) - if (SuppressSurveyConfirmation || viewModel.isSurvey(geometry)) { - viewModel.submitEdit( - elementEditType = shownBottomSheet.overlay, - geometry = geometry, - elementEditAction = action + if (SuppressSurveyConfirmation || isSurvey(geometry)) { + onSubmitEdit( + shownBottomSheet.overlay, + geometry, + action ) onSolved(shownBottomSheet.overlay.icon, geometry.center) onDismiss() @@ -158,10 +165,11 @@ fun MainBottomSheet( }, onLeaveNote = { noteText, noteImagePaths -> val center = shownBottomSheet.geometry?.center ?: mapPosition - viewModel.createNote( - position = center, - text = noteText, - imagePaths = noteImagePaths, + onCreateNote( + center, + noteText, + noteImagePaths, + null, ) onSolved(shownBottomSheet.overlay.icon, center) onDismiss() @@ -186,10 +194,10 @@ fun MainBottomSheet( SurveyConfirmationDialog( onDismissRequest = { confirmEdit = null }, onConfirmed = { - viewModel.submitEdit( - elementEditType = pendingEdit.elementEditType, - geometry = pendingEdit.geometry, - elementEditAction = pendingEdit.elementEditAction + onSubmitEdit( + pendingEdit.elementEditType, + pendingEdit.geometry, + pendingEdit.elementEditAction ) onDismiss() }, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/AttributionButton.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/AttributionButton.kt index 47584c04bf3..1568f19d9f3 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/AttributionButton.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/AttributionButton.kt @@ -7,8 +7,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.absolutePadding @@ -18,9 +16,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme -import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.LocalContentColor import androidx.compose.material.Surface -import androidx.compose.material.Text import androidx.compose.material.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,12 +31,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup @@ -48,6 +42,7 @@ import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.screens.main.controls.ktx.Reverse import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.maplibre.compose.overlay.AttributionLinks /** * Info button from which an attribution popup text is expanded from. The attribution text retracts @@ -70,7 +65,7 @@ import org.jetbrains.compose.resources.stringResource @Composable public fun AttributionButton( userHasMovedMap: Boolean, - attributions: List, + attributions: List, modifier: Modifier = Modifier, textStyle: TextStyle = MaterialTheme.typography.body2, textLinkStyles: TextLinkStyles? = null, @@ -111,7 +106,7 @@ public fun AttributionButton( val horizontalArrangement = if (alignLeft) Arrangement.Absolute.Left else Arrangement.Absolute.Reverse val textAlign = if (alignLeft) TextAlign.Left else TextAlign.Right - val attributionAlignment = if (alignLeft) AbsoluteAlignment.Left else AbsoluteAlignment.Right + val attributionAlignment = if (alignLeft) AbsoluteAlignment.TopLeft else AbsoluteAlignment.TopRight Popup( popupPositionProvider = popupPositionProvider, @@ -144,14 +139,18 @@ public fun AttributionButton( ) { InfoIcon() } - AttributionTexts( - attributions = attributions, - textStyle = textStyle, - textLinkStyles = textLinkStyles, - textAlign = textAlign, - attributionAlignment = attributionAlignment, - modifier = Modifier.padding(vertical = 8.dp), - ) + Box(contentAlignment = attributionAlignment) { + AttributionLinks( + attributions = attributions, + textStyle = textStyle.copy( + color = LocalContentColor.current, + textAlign = textAlign, + ), + linkStyles = textLinkStyles, + breakWithinAttribution = true, + modifier = Modifier.padding(vertical = 8.dp), + ) + } // icon buttons are automatically padded to have a certain click size, which makes the // popup appear misaligned if we don't also add some extra padding on the other side Spacer(Modifier.width(16.dp)) @@ -177,31 +176,3 @@ private fun InfoIcon(modifier: Modifier = Modifier) { modifier = modifier, ) } - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun AttributionTexts( - attributions: List, - textStyle: TextStyle, - textLinkStyles: TextLinkStyles?, - textAlign: TextAlign, - attributionAlignment: Alignment.Horizontal, - modifier: Modifier = Modifier, -) { - ProvideTextStyle(textStyle) { - FlowRow( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp, attributionAlignment) - ) { - attributions.distinct().forEach { - val attributionString = buildAnnotatedString { - val link = LinkAnnotation.Url(url = it.url, styles = textLinkStyles) - withLink(link) { append(it.title) } - } - Text(text = attributionString, textAlign = textAlign) - } - } - } -} - -data class AttributionLink(val title: String, val url: String) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Intersection.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Intersection.kt deleted file mode 100644 index d977d5c8da5..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Intersection.kt +++ /dev/null @@ -1,36 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.controls - -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import kotlin.math.PI -import kotlin.math.atan2 -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.sqrt - -/** Given an imaginary line drawn from the center of [screen] to [target], returns the point and - * angle at which the line intersects with an ellipsis filling the [screen]. */ -fun findEllipsisIntersection( - screen: Rect?, - target: Offset? -): Intersection? { - val t = target ?: return null - val s = screen ?: return null - val o = s.center - val delta = t - o - - val theta = atan2(delta.y, delta.x) + PI / 2 - val radius = ellipsisRadius(s.height / 2.0, s.width / 2.0, theta) - val ellipsisDelta = Offset((sin(theta) * radius).toFloat(), (-cos(theta) * radius).toFloat()) - if (delta.getDistanceSquared() < ellipsisDelta.getDistanceSquared()) return null - - return Intersection(ellipsisDelta + o, theta) -} - -private fun ellipsisRadius(a: Double, b: Double, angle: Double): Double { - val x = sin(angle) - val y = cos(angle) - return a * b / sqrt(a * a * x * x + b * b * y * y) -} - -data class Intersection(val position: Offset, val angle: Double) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/MainScreenControls.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/MainScreenControls.kt index c16ac4ef395..8bfbaa0059b 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/MainScreenControls.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/MainScreenControls.kt @@ -1,14 +1,12 @@ package de.westnordost.streetcomplete.screens.main.controls import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -27,22 +25,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.layout.boundsInRoot -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.data.overlays.Overlay -import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.screens.main.overlays.OverlaySelectionDropdownMenu import de.westnordost.streetcomplete.ui.common.LargeCreateIcon import de.westnordost.streetcomplete.ui.common.StopRecordingIcon import de.westnordost.streetcomplete.ui.common.UndoIcon -import de.westnordost.streetcomplete.ui.ktx.pxToDp -import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.stringResource -import kotlin.math.PI /** Map controls shown on top of the map, a.k.a. the map HUD */ @Composable @@ -79,13 +68,11 @@ fun MainScreenControls( mapTilt: Float, onClickCompass: () -> Unit, - // location button & pointer + // location button locationState: LocationState?, isNavigationMode: Boolean, isFollowingPosition: Boolean, - displayedLocationOffset: Offset?, onClickLocation: () -> Unit, - onClickLocationPointer: () -> Unit, // (stop) record track button isRecordingTracks: Boolean, @@ -104,22 +91,13 @@ fun MainScreenControls( metersPerDp: Double, // attribution button + attributions: List, userHasMovedMap: Boolean, modifier: Modifier = Modifier, ) { - val mapAttribution = listOf( - AttributionLink(stringResource(Res.string.map_attribution_osm), "https://osm.org/copyright"), - AttributionLink("© JawgMaps", "https://jawg.io") - ) - var showOverlaysDropdown by remember { mutableStateOf(false) } - var screen by remember { mutableStateOf(null) } - val intersection = remember(displayedLocationOffset, screen) { - findEllipsisIntersection(screen, displayedLocationOffset) - } - // color for HUD elements without a background (e.g. scalebar, attribution button) CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onSurface) { Box(modifier @@ -129,19 +107,9 @@ fun MainScreenControls( Crosshair() } - intersection?.let { (offset, angle) -> - val rotation = angle * 180 / PI - PointerPinButton( - onClick = onClickLocationPointer, - rotate = rotation.toFloat(), - modifier = Modifier.absoluteOffset(offset.x.pxToDp(), offset.y.pxToDp()), - ) { Image(painterResource(Res.drawable.location_dot_small), null) } - } - Box(Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing) - .onGloballyPositioned { screen = it.boundsInRoot() } ) { // top-start controls @@ -274,7 +242,7 @@ fun MainScreenControls( Box(Modifier.fillMaxWidth().padding(4.dp)) { AttributionButton( userHasMovedMap = userHasMovedMap, - attributions = mapAttribution, + attributions = attributions, modifier = Modifier.align(Alignment.TopStart), popupElevation = 4.dp, textLinkStyles = TextLinkStyles() diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/PointerPinButton.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/PointerPinButton.kt index cf67551556d..41ef05eb833 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/PointerPinButton.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/PointerPinButton.kt @@ -1,12 +1,6 @@ package de.westnordost.streetcomplete.screens.main.controls -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding @@ -16,36 +10,30 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Matrix import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.PathParser import androidx.compose.ui.graphics.vector.toPath -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import de.westnordost.streetcomplete.resources.* -import de.westnordost.streetcomplete.ui.ktx.proportionalAbsoluteOffset import de.westnordost.streetcomplete.ui.ktx.proportionalPadding import de.westnordost.streetcomplete.ui.theme.divider -import org.jetbrains.compose.resources.painterResource -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin +import org.maplibre.compose.overlay.MapOverlayScope +import org.maplibre.compose.overlay.rememberPlacedTowardsState +import org.maplibre.spatialk.geojson.Position -/** A view for the pointer pin that ought to be displayed at the edge of the screen. The upper left - * corner is always the position at which it is pointing to, i.e. it will be drawn outside of - * its bounds when pointing to the right. - * [rotate] rotates the pin. As opposed to normal rotation, the content always stays upright */ +/** A pointer at the edge of the unobstructed map, shown while [targetPosition] lies outside + * its inscribed ellipse. The pin points towards the target; its content stays upright. */ @OptIn(ExperimentalMaterialApi::class) @Composable -fun PointerPinButton( +fun MapOverlayScope.PointerPinButton( + targetPosition: Position, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, @@ -53,34 +41,31 @@ fun PointerPinButton( backgroundColor = MaterialTheme.colors.surface, ), contentPadding: Dp = 12.dp, - rotate: Float = 0f, content: @Composable (BoxScope.() -> Unit), ) { - val pointerPinShape = remember(rotate) { PointerPinShape(rotate) } - val a = rotate * PI / 180f - val pointySize = 14f / 76f + val placement = rememberPlacedTowardsState() Surface( onClick = onClick, modifier = modifier - .proportionalAbsoluteOffset( - x = (-sin(a) / 2.0 - 0.5).toFloat(), - y = (cos(a) / 2.0 - 0.5).toFloat(), - ), + .placedTowards(targetPosition, placement) + // Placement sets the angle during layout; draw-time rotation uses it in the same frame. + .graphicsLayer { rotationZ = placement.angleDegrees }, enabled = enabled, - shape = pointerPinShape, + shape = PointerPinShape, color = colors.backgroundColor(enabled).value, contentColor = colors.contentColor(enabled).value, border = BorderStroke(1.dp, MaterialTheme.colors.divider), elevation = 4.dp ) { Box(Modifier - .proportionalPadding(pointySize) + .graphicsLayer { rotationZ = -placement.angleDegrees } + .proportionalPadding(14f / 76f) .padding(contentPadding) ) { content() } } } -private class PointerPinShape(val rotation: Float = 0f) : Shape { +private object PointerPinShape : Shape { private val pathSize = 76f private val path = PathParser() @@ -93,11 +78,6 @@ private class PointerPinShape(val rotation: Float = 0f) : Shape { density: Density ): Outline { val m = Matrix() - val halfWidth = size.width / 2 - val halfHeight = size.height / 2 - m.translate(halfWidth, halfHeight) - m.rotateZ(rotation) - m.translate(-halfWidth, -halfHeight) m.scale( x = size.width / pathSize, y = size.height / pathSize @@ -107,16 +87,3 @@ private class PointerPinShape(val rotation: Float = 0f) : Shape { return Outline.Generic(p) } } - -@Preview -@Composable -private fun PreviewPointerPinButton() { - val infiniteTransition = rememberInfiniteTransition() - val rotation by infiniteTransition.animateFloat( - 0f, 360f, - infiniteRepeatable(tween(12000, 0, LinearEasing)), - ) - PointerPinButton(onClick = {}, rotate = rotation) { - Image(painterResource(Res.drawable.location_dot_small), null) - } -} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBar.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBar.kt index 7062419710f..06a9d9ccf86 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBar.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBar.kt @@ -1,35 +1,17 @@ package de.westnordost.streetcomplete.screens.main.controls -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size import androidx.compose.material.MaterialTheme import androidx.compose.material.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.toSize -import de.westnordost.streetcomplete.screens.main.controls.ktx.drawPathsWithHalo -import de.westnordost.streetcomplete.screens.main.controls.ktx.drawTextWithHalo -import kotlin.math.ceil -import kotlin.math.roundToInt - -/** Which measures to show on the scale bar. */ -public data class ScaleBarMeasures( - val primary: ScaleBarMeasure, - val secondary: ScaleBarMeasure? = null, -) +import org.maplibre.compose.overlay.ScaleBar +import org.maplibre.compose.overlay.ScaleBarDefaults +import org.maplibre.compose.overlay.ScaleBarMeasures /** * A scale bar composable that shows the current scale of the map in feet, meters or feet and meters @@ -37,13 +19,13 @@ public data class ScaleBarMeasures( * * @param metersPerDp how many meters are displayed in one device independent pixel (dp), i.e. the * scale. See - * [CameraState.metersPerDpAtTarget][dev.sargunv.maplibrecompose.compose.CameraState.metersPerDpAtTarget] + * [Viewport.metersPerDpAtTarget][org.maplibre.compose.camera.Viewport.metersPerDpAtTarget] * @param modifier the [Modifier] to be applied to this layout node - * @param measures which measures to show on the scale bar. If `null`, measures will be selected - * based on the system settings or otherwise the user's locale. + * @param measures which measures to show on the scale bar. The default follows the system settings, + * or otherwise the user's locale. + * @param color scale bar and text color. * @param haloColor halo for better visibility when displayed on top of the map * @param haloWidth scale bar and text halo width - * @param color scale bar and text color. * @param barWidth scale bar width * @param textStyle the text style. The text size is the deciding factor how large the scale bar is * is displayed. @@ -53,158 +35,23 @@ public data class ScaleBarMeasures( fun ScaleBar( metersPerDp: Double, modifier: Modifier = Modifier, - measures: ScaleBarMeasures = defaultScaleBarMeasures(), + measures: ScaleBarMeasures = ScaleBarDefaults.measures(), haloColor: Color = MaterialTheme.colors.surface, - haloWidth: Dp = 0.dp, color: Color = contentColorFor(haloColor), - barWidth: Dp = 2.dp, + haloWidth: Dp = 0.dp, + barWidth: Dp = ScaleBarDefaults.BarWidth, textStyle: TextStyle = MaterialTheme.typography.caption, alignment: Alignment.Horizontal = Alignment.Start, ) { - // when map is not fully initialized yet - if (metersPerDp == 0.0) return - - val textMeasurer = rememberTextMeasurer() - // longest possible text - val maxTextSizePx = - remember(textMeasurer, textStyle) { textMeasurer.measure("5000 km", textStyle).size } - val maxTextSize = with(LocalDensity.current) { maxTextSizePx.toSize().toDpSize() } - - // padding of text to bar stroke - val textHorizontalPadding = 4.dp - val textVerticalPadding = 0.dp - - // multiplied by 2.5 because the next stop can be the x2.5 of a previous stop (e.g. 2km -> 5km), - // so the bar can end at approx 1/2.5th of the total width. We want to avoid that the bar - // intersects with the text, i.e. is drawn behind the text - val totalMaxWidth = maxTextSize.width * 2.5f + (textHorizontalPadding + barWidth) * 2f - - val fullStrokeWidth = haloWidth * 2 + barWidth - - val textCount = if (measures.secondary != null) 2 else 1 - val totalHeight = (maxTextSize.height + textVerticalPadding) * textCount + fullStrokeWidth - - BoxWithConstraints(modifier.size(totalMaxWidth, totalHeight)) { - // scale bar start/end should not overlap horizontally with canvas bounds - val maxBarLength = maxWidth - fullStrokeWidth - - val params1 = scaleBarParameters(measures.primary, metersPerDp, maxBarLength) - val params2 = measures.secondary?.let { scaleBarParameters(it, metersPerDp, maxBarLength) } - - Canvas(Modifier.fillMaxSize()) { - val fullStrokeWidthPx = fullStrokeWidth.toPx() - val textHeightPx = maxTextSizePx.height - val textHorizontalPaddingPx = textHorizontalPadding.toPx() - val textVerticalPaddingPx = textVerticalPadding.toPx() - - // bar ends should go to the vertical center of the text - val barEndsHeightPx = textHeightPx / 2f + textVerticalPadding.toPx() + fullStrokeWidthPx / 2f - - var y = 0f - val paths = ArrayList>(2) - val texts = ArrayList>(2) - - val alignmentSpacePx = ceil(size.width - fullStrokeWidthPx).toInt() - - if (true) { // just want a scope here - val barLengthPx = params1.barLength.toPx().roundToInt() - val offsetX = - alignment.align( - size = barLengthPx, - space = alignmentSpacePx, - layoutDirection = layoutDirection, - ) - paths.add( - listOf( - Offset(offsetX + fullStrokeWidthPx / 2f, 0f + textHeightPx / 2f), - Offset(0f, barEndsHeightPx), - Offset(barLengthPx.toFloat(), 0f), - Offset(0f, -barEndsHeightPx), - ) - ) - texts.add( - Pair( - Offset(textHorizontalPaddingPx + fullStrokeWidthPx, 0f), - textMeasurer.measure(params1.text, textStyle), - ) - ) - - y += textHeightPx + textVerticalPaddingPx - } - - if (params2 != null) { - val barLengthPx = params2.barLength.toPx().roundToInt() - val offsetX = - alignment.align( - size = barLengthPx, - space = alignmentSpacePx, - layoutDirection = layoutDirection, - ) - paths.add( - listOf( - Offset(offsetX + fullStrokeWidthPx / 2f, y + fullStrokeWidthPx / 2f + barEndsHeightPx), - Offset(0f, -barEndsHeightPx), - Offset(barLengthPx.toFloat(), 0f), - Offset(0f, +barEndsHeightPx), - ) - ) - texts.add( - Pair( - Offset( - textHorizontalPaddingPx + fullStrokeWidthPx, - y + textVerticalPaddingPx + fullStrokeWidthPx, - ), - textMeasurer.measure(params2.text, textStyle), - ) - ) - } - - drawPathsWithHalo( - color = color, - haloColor = haloColor, - paths = paths, - strokeWidth = barWidth.toPx(), - haloWidth = haloWidth.toPx(), - cap = StrokeCap.Round, - ) - - for ((offset, textLayoutResult) in texts) { - val offsetX = - alignment.align( - size = textLayoutResult.size.width, - space = ceil(size.width - 2 * offset.x).toInt(), - layoutDirection = layoutDirection, - ) + offset.x - drawTextWithHalo( - textLayoutResult = textLayoutResult, - topLeft = Offset(offsetX, offset.y), - color = color, - haloColor = haloColor, - haloWidth = haloWidth.toPx(), - ) - } - } - } -} - -private data class ScaleBarParams(val barLength: Dp, val text: String) - -@Composable -private fun scaleBarParameters( - measure: ScaleBarMeasure, - metersPerDp: Double, - maxBarLength: Dp, -): ScaleBarParams { - val max = maxBarLength.value * metersPerDp / measure.unitInMeters - val stop = findStop(max, measure.stops) - return ScaleBarParams((stop * measure.unitInMeters / metersPerDp).dp, measure.getText(stop)) -} - -/** - * find the largest stop in the list of stops (sorted in ascending order) that is below or equal - * [max]. - */ -private fun findStop(max: Double, stops: List): Double { - val i = stops.binarySearch { it.compareTo(max) } - return if (i >= 0) stops[i] else stops[(-i - 2).coerceAtLeast(0)] + ScaleBar( + metersPerDp = metersPerDp, + modifier = modifier, + measures = measures, + color = color, + haloColor = haloColor, + haloWidth = haloWidth, + barWidth = barWidth, + textStyle = textStyle, + alignment = alignment + ) } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.kt deleted file mode 100644 index bb8ecbac6e4..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.kt +++ /dev/null @@ -1,173 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.controls - -import androidx.compose.runtime.Composable -import androidx.compose.ui.text.intl.Locale -import de.westnordost.streetcomplete.resources.* -import de.westnordost.streetcomplete.screens.main.controls.ScaleBarMeasure.FeetAndMiles -import de.westnordost.streetcomplete.screens.main.controls.ScaleBarMeasure.Metric -import de.westnordost.streetcomplete.screens.main.controls.ScaleBarMeasure.YardsAndMiles -import org.jetbrains.compose.resources.stringResource -import kotlin.math.pow - -/** A measure to show in the scale bar */ -interface ScaleBarMeasure { - /** one unit of this measure in meters */ - val unitInMeters: Double - - /** List of stops, sorted ascending, at which the scale bar should show */ - val stops: List - - @Composable public fun getText(stop: Double): String - - /** A measure of meters and kilometers */ - data object Metric : ScaleBarMeasure { - override val unitInMeters: Double = 1.0 - - override val stops: List = buildStops(mantissas = listOf(1, 2, 5), exponents = -1..7) - - @Composable - override fun getText(stop: Double): String = - if (stop >= 1000) { - (stop / 1000).formatForDisplay(stringResource(Res.string.kilometers_symbol)) - } else { - stop.formatForDisplay(stringResource(Res.string.meters_symbol)) - } - } - - /** A measure of international feet and miles */ - data object FeetAndMiles : ScaleBarMeasure { - - private const val FEET_IN_MILE: Int = 5280 - - override val unitInMeters: Double = 0.3048 - - override val stops: List = - listOf( - buildStops(mantissas = listOf(1, 2, 5), exponents = -1..3).dropLast(1), - buildStops(mantissas = listOf(1, 2, 5), exponents = 0..4).map { it * FEET_IN_MILE }, - ) - .flatten() - - @Composable - override fun getText(stop: Double): String = - if (stop >= FEET_IN_MILE) { - (stop / FEET_IN_MILE).formatForDisplay(stringResource(Res.string.miles_symbol)) - } else { - stop.formatForDisplay(stringResource(Res.string.feet_symbol)) - } - } - - /** A measure of international yard and miles */ - data object YardsAndMiles : ScaleBarMeasure { - - private const val YARDS_IN_MILE: Int = 1760 - - override val unitInMeters: Double = 0.9144 - - override val stops: List = - listOf( - buildStops(mantissas = listOf(1, 2, 5), exponents = -1..3).dropLast(2), - buildStops(mantissas = listOf(1, 2, 5), exponents = 0..4).map { it * YARDS_IN_MILE }, - ) - .flatten() - - @Composable - override fun getText(stop: Double): String = - if (stop >= YARDS_IN_MILE) { - (stop / YARDS_IN_MILE).formatForDisplay(stringResource(Res.string.miles_symbol)) - } else { - stop.formatForDisplay(stringResource(Res.string.yards_symbol)) - } - } -} - -/** format a number with a unit symbol, not showing the decimal point if it's an integer */ -private fun Double.formatForDisplay(symbol: String) = - if (this.toInt().toDouble() == this) "${this.toInt()} $symbol" else "$this $symbol" - -/** build a list of stops by multiplying mantissas by 10^exponents, like scientific notation */ -private fun buildStops(mantissas: List, exponents: IntRange) = buildList { - for (e in exponents) for (m in mantissas) add(m * 10.0.pow(e)) -} - -/** use system locale APIs for the primary scale bar measure */ -@Composable internal expect fun systemDefaultPrimaryMeasure(): ScaleBarMeasure? - -/** if the system APIs don't provide a primary measure, fall back to our hardcoded lists */ -internal fun fallbackDefaultPrimaryMeasure(region: String?): ScaleBarMeasure = - when (region) { - in regionsUsingFeetAndMiles -> FeetAndMiles - in regionsUsingYardsAndMiles -> YardsAndMiles - else -> Metric - } - -/** countries using non-metric units will see both systems by default */ -internal fun defaultSecondaryMeasure(primary: ScaleBarMeasure, region: String?): ScaleBarMeasure? = - when (primary) { - FeetAndMiles -> Metric - YardsAndMiles -> Metric - Metric -> - when (region) { - in regionsUsingFeetAndMiles -> FeetAndMiles - in regionsUsingYardsAndMiles -> YardsAndMiles - else -> null - } - else -> null // should never happen because the primary is always one of the above - } - -private val regionsUsingFeetAndMiles = - setOf( - // United states and its unincorporated territories - "US", - "AS", - "GU", - "MP", - "PR", - "VI", - // former United states territories / Compact of Free Association - "FM", - "MH", - "PW", - // Liberia - "LR", - ) - -private val regionsUsingYardsAndMiles = - setOf( - // United kingdom with its overseas territories and crown dependencies - "GB", - "AI", - "BM", - "FK", - "GG", - "GI", - "GS", - "IM", - "IO", - "JE", - "KY", - "MS", - "PN", - "SH", - "TC", - "VG", - // former British overseas territories / colonies - "BS", - "BZ", - "GD", - "KN", - "VC", - // Myanmar - "MM", - ) - -/** - * default scale bar measures to use, depending on the user's locale (or system preferences, if - * available) - */ -@Composable -internal fun defaultScaleBarMeasures(): ScaleBarMeasures { - val region = Locale.current.region - val primary = systemDefaultPrimaryMeasure() ?: fallbackDefaultPrimaryMeasure(region) - return ScaleBarMeasures(primary = primary, secondary = defaultSecondaryMeasure(primary, region)) -} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ktx/DrawScope.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ktx/DrawScope.kt deleted file mode 100644 index ab71fd97a4e..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ktx/DrawScope.kt +++ /dev/null @@ -1,86 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.controls.ktx - -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.PathEffect -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.StrokeJoin -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultBlendMode -import androidx.compose.ui.graphics.drawscope.Fill -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.drawText - -/** Draw several lines. Each offset in [path] is relative to the previous. */ -internal fun DrawScope.drawPath( - color: Color, - path: List, - strokeWidth: Float = Stroke.HairlineWidth, - cap: StrokeCap = Stroke.DefaultCap, - pathEffect: PathEffect? = null, - alpha: Float = 1.0f, - colorFilter: ColorFilter? = null, - blendMode: BlendMode = DefaultBlendMode, -) { - val it = path.iterator() - if (!it.hasNext()) return - var start = it.next() - while (it.hasNext()) { - val end = start + it.next() - drawLine( - color = color, - start = start, - end = end, - strokeWidth = strokeWidth, - cap = cap, - pathEffect = pathEffect, - alpha = alpha, - colorFilter = colorFilter, - blendMode = blendMode, - ) - start = end - } -} - -/** Draw several paths with halo. All halos of all [paths] are behind all strokes. */ -internal fun DrawScope.drawPathsWithHalo( - color: Color, - haloColor: Color, - paths: List>, - strokeWidth: Float = Stroke.HairlineWidth, - haloWidth: Float = Stroke.HairlineWidth, - cap: StrokeCap = Stroke.DefaultCap, -) { - if (haloWidth > 0f && haloColor != Color.Transparent) { - for (path in paths) { - drawPath(color = haloColor, path = path, strokeWidth = strokeWidth + haloWidth * 2, cap = cap) - } - } - for (path in paths) { - drawPath(color = color, path = path, strokeWidth = strokeWidth, cap = cap) - } -} - -internal fun DrawScope.drawTextWithHalo( - textLayoutResult: TextLayoutResult, - topLeft: Offset = Offset.Zero, - color: Color = Color.Unspecified, - haloColor: Color = Color.Unspecified, - haloWidth: Float = 0f, -) { - // * 2 because the stroke is painted half outside and half inside of the text shape - if (haloWidth > 0f && haloColor != Color.Transparent) { - // * 2 because the stroke is painted half outside and half inside of the text shape - val stroke = Stroke(width = haloWidth * 2, cap = StrokeCap.Round, join = StrokeJoin.Round) - drawText( - textLayoutResult = textLayoutResult, - color = haloColor, - topLeft = topLeft, - drawStyle = stroke, - ) - } - drawText(textLayoutResult = textLayoutResult, color = color, topLeft = topLeft, drawStyle = Fill) -} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/edithistory/EditHistoryViewModel.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/edithistory/EditHistoryViewModel.kt index b7b746d43e7..a09bb2ccede 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/edithistory/EditHistoryViewModel.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/edithistory/EditHistoryViewModel.kt @@ -31,21 +31,13 @@ import kotlin.time.Instant @Stable abstract class EditHistoryViewModel : ViewModel() { - abstract val editItems: StateFlow> - abstract val selectedEdit: StateFlow + abstract val editItems: StateFlow?> abstract suspend fun getEditElement(edit: Edit): Element? abstract suspend fun getEditGeometry(edit: Edit): ElementGeometry - abstract fun select(editKey: EditKey?) abstract fun undo(editKey: EditKey) - /* edit sidebar */ - // TODO could maybe be just a boolean in the composable when there's no communication between - // compose <-> fragment communication necessary anymore - abstract fun showSidebar() - abstract fun hideSidebar() - abstract val isShowingSidebar: StateFlow } data class EditItem( @@ -60,14 +52,12 @@ class EditHistoryViewModelImpl( private val editHistoryController: EditHistoryController, ) : EditHistoryViewModel() { - private val edits = MutableStateFlow>(emptyList()) - - override val selectedEdit = MutableStateFlow(null) + private val edits = MutableStateFlow?>(null) @OptIn(ExperimentalCoroutinesApi::class) override val editItems = edits - .transformLatest { emit(it.toEditItems()) } - .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + .transformLatest { emit(it?.toEditItems()) } + .stateIn(viewModelScope, SharingStarted.Lazily, null) override suspend fun getEditElement(edit: Edit): Element? { val key = edit.primaryElementKey ?: return null @@ -80,36 +70,16 @@ class EditHistoryViewModelImpl( else -> null } ?: ElementPointGeometry(edit.position) - override fun select(editKey: EditKey?) { - selectedEdit.value = - if (editKey != null) { - edits.value.firstOrNull { it.key == editKey } - } else { - null - } - } - override fun undo(editKey: EditKey) { launch(Dispatchers.IO) { editHistoryController.undo(editKey) } } - override fun showSidebar() { - selectedEdit.value = edits.value.lastOrNull() - isShowingSidebar.value = true - } - - override fun hideSidebar() { - selectedEdit.value = null - isShowingSidebar.value = false - } - - override val isShowingSidebar = MutableStateFlow(false) - private val editHistoryListener = object : EditHistorySource.Listener { override fun onAdded(added: Edit) { edits.update { edits -> + val edits = edits.orEmpty() var insertIndex = edits.indexOfLast { it.createdTimestamp > added.createdTimestamp } if (insertIndex == -1) insertIndex = edits.size edits.toMutableList().also { it.add(insertIndex, added) } @@ -117,10 +87,8 @@ class EditHistoryViewModelImpl( } override fun onSynced(synced: Edit) { - if (selectedEdit.value?.key == synced.key) { - selectedEdit.value = synced - } edits.update { edits -> + val edits = edits.orEmpty() val editIndex = edits.indexOfLast { it.key == synced.key } if (editIndex != -1) { edits.toMutableList().also { it[editIndex] = synced } @@ -132,13 +100,9 @@ class EditHistoryViewModelImpl( override fun onDeleted(deleted: List) { val deletedKeys = deleted.mapTo(HashSet()) { it.key } - if (selectedEdit.value?.key in deletedKeys) { - selectedEdit.value = null - } edits.update { edits -> - edits.filter { it.key !in deletedKeys } + edits?.filter { it.key !in deletedKeys } } - if (edits.value.isEmpty()) hideSidebar() } override fun onInvalidated() { @@ -158,7 +122,6 @@ class EditHistoryViewModelImpl( private fun updateEdits() { launch(Dispatchers.IO) { edits.value = editHistoryController.getAll().sortedBy { it.createdTimestamp } - if (edits.value.isEmpty()) hideSidebar() } } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/AnimationUtils.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/AnimationUtils.kt new file mode 100644 index 00000000000..415fd6697e6 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/AnimationUtils.kt @@ -0,0 +1,54 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.animation.core.Spring.StiffnessLow +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.animateOffsetAsState +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.util.math.normalizeLongitude + +/** Animates a LatLon to a [targetValue] position. Also works when crossing the antimeridian. */ +@Composable +fun animateLatLonAsState( + targetValue: LatLon, + animationSpec: SpringSpec = spring(stiffness = StiffnessLow), + label: String = "LatLonAnimation" +): State { + val origin = remember { targetValue } + var targetLongitude by remember { mutableStateOf(targetValue.longitude) } + + LaunchedEffect(targetValue.longitude) { + targetLongitude += normalizeLongitude(targetValue.longitude - targetLongitude) + } + + // Animate local deltas so Float animation vectors retain precision for small GPS movements. + val animatedOffset by animateOffsetAsState( + targetValue = Offset( + (targetValue.latitude - origin.latitude).toFloat(), + (targetLongitude - origin.longitude).toFloat(), + ), + animationSpec = spring( + dampingRatio = animationSpec.dampingRatio, + stiffness = animationSpec.stiffness, + visibilityThreshold = Offset( + (animationSpec.visibilityThreshold?.latitude ?: 1e-7).toFloat(), + (animationSpec.visibilityThreshold?.longitude ?: 1e-7).toFloat(), + ), + ), + label = label, + ) + + return remember { derivedStateOf { LatLon( + latitude = origin.latitude + animatedOffset.x, + longitude = normalizeLongitude(origin.longitude + animatedOffset.y), + ) } } +} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/Bearing.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/Bearing.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/Bearing.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/Bearing.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/ExpressionUtils.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/ExpressionUtils.kt new file mode 100644 index 00000000000..6bea444d39b --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/ExpressionUtils.kt @@ -0,0 +1,136 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import org.maplibre.compose.expressions.ast.Expression +import org.maplibre.compose.expressions.dsl.Feature +import org.maplibre.compose.expressions.dsl.all +import org.maplibre.compose.expressions.dsl.any +import org.maplibre.compose.expressions.dsl.coalesce +import org.maplibre.compose.expressions.dsl.condition +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.contains +import org.maplibre.compose.expressions.dsl.convertToBoolean +import org.maplibre.compose.expressions.dsl.convertToNumber +import org.maplibre.compose.expressions.dsl.convertToString +import org.maplibre.compose.expressions.dsl.div +import org.maplibre.compose.expressions.dsl.dp +import org.maplibre.compose.expressions.dsl.eq +import org.maplibre.compose.expressions.dsl.exponential +import org.maplibre.compose.expressions.dsl.interpolate +import org.maplibre.compose.expressions.dsl.neq +import org.maplibre.compose.expressions.dsl.plus +import org.maplibre.compose.expressions.dsl.switch +import org.maplibre.compose.expressions.dsl.times +import org.maplibre.compose.expressions.dsl.zoom +import org.maplibre.compose.expressions.value.GeometryType +import org.maplibre.compose.expressions.value.NumberValue +import org.maplibre.compose.expressions.value.StringValue +import kotlin.jvm.JvmName +import kotlin.math.PI +import kotlin.math.cos + +fun fadeInAtZoom(start: Float, range: Float = 1f, endOpacity: Float = 1f) = + byZoom(start to 0f, start+range to endOpacity) + +fun fadeOutAtZoom(start: Float, range: Float = 1f, startOpacity: Float = 1f) = + byZoom(start to startOpacity, start+range to 0f) + +@JvmName("byZoomFloat") +fun byZoom(vararg stops: Pair) = + interpolate(exponential(2f), zoom(), *stops.map { it.first to const(it.second) }.toTypedArray()) + +@JvmName("byZoomDp") +fun byZoom(vararg stops: Pair) = + interpolate(exponential(2f), zoom(), *stops.map { it.first to const(it.second) }.toTypedArray()) + +@JvmName("byZoomTextUnit") +fun byZoom(vararg stops: Pair) = + interpolate(exponential(2f), zoom(), *stops.map { it.first to const(it.second) }.toTypedArray()) + +/** Returns whether this feature has the given [key]-[value] pair */ +fun Feature.has(key: String, value: String) = + get(key).convertToString() eq const(value) + +/** Returns whether this feature has the given [key]-[value] pair */ +fun Feature.has(key: String, value: Int) = + get(key).convertToNumber() eq const(value) + +/** Returns whether this feature has the given [key]-[value] pair */ +fun Feature.has(key: String, value: Boolean) = + get(key).convertToBoolean() eq const(value) + +/** Returns whether this feature has a [key]-value pair of which the value is in of the given + * [values] */ +fun Feature.hasAny(key: String, values: List) = + const(values).contains(get(key)) + +fun Feature.isPoint() = + geometryType() eq const(GeometryType.Point) + +fun Feature.isLines() = + any( + geometryType() eq const(GeometryType.LineString), + geometryType() eq const(GeometryType.MultiLineString) + ) + +fun Feature.isArea() = + any( + geometryType() eq const(GeometryType.Polygon), + geometryType() eq const(GeometryType.MultiPolygon) + ) + +/** Get an expression that resolves to the localized name. + * If the localized name in the user's [languages] is the same as the primary name, then only this + * name is displayed. Otherwise, the primary name is displayed, then the localized name below */ +fun Feature.localizedName( + languages: List, + nameKey: String, + localizedNameKey: (String) -> String, + extraNameKeys: List +): Expression { + val localizedNameKeys = languages.map(localizedNameKey) + extraNameKeys + val getLocalizedName = if (localizedNameKeys.isEmpty()) const("") else + coalesce(*localizedNameKeys.map { get(it) }.toTypedArray()).convertToString() + val getName = get(nameKey).convertToString() + return switch( + // localized name set and different as main name -> show both + condition( + test = all(getLocalizedName.convertToBoolean(), getName neq getLocalizedName), + output = getName + const("\n") + getLocalizedName + ), + // otherwise just show the name + fallback = getName + ) +} + +fun inMeters( + width: Expression>, + latitude: Double = 30.0 +): Expression> { + val sizeFactor = metersSizeFactor(latitude) + return interpolate( + exponential(2f), zoom(), + 8 to width / const(256) / const(sizeFactor), + 24 to width * const(256) / const(sizeFactor) + ).dp +} + +fun inMeters( + width: Float, + latitude: Double = 30.0 +): Expression> { + val sizeFactor = metersSizeFactor(latitude) + return interpolate( + exponential(2f), zoom(), + 8 to const(width) / const(256) / const(sizeFactor), + 24 to const(width) * const(256) / const(sizeFactor) + ).dp +} + +// the more north you go, the smaller of an area each mercator tile actually covers +// the additional factor of 1.20 comes from a simple measuring test with a ruler on a +// smartphone screen done at approx. latitude = 0 and latitude = 70, i.e. without it, lines are +// drawn at both latitudes approximately 20% too large ¯\_(ツ)_/¯ +private fun metersSizeFactor(latitude: Double): Float = + (cos(PI * latitude / 180) * 1.2).toFloat() diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/GeometryUtils.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/GeometryUtils.kt new file mode 100644 index 00000000000..10a40cfa5be --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/GeometryUtils.kt @@ -0,0 +1,105 @@ +package de.westnordost.streetcomplete.screens.main.map + +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry +import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry +import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.util.math.isInPolygon +import de.westnordost.streetcomplete.util.math.isRingDefinedClockwise +import de.westnordost.streetcomplete.util.math.measuredArea +import org.maplibre.compose.util.VisibleBounds +import org.maplibre.spatialk.geojson.LineString +import org.maplibre.spatialk.geojson.MultiLineString +import org.maplibre.spatialk.geojson.MultiPolygon +import org.maplibre.spatialk.geojson.Point +import org.maplibre.spatialk.geojson.Polygon +import org.maplibre.spatialk.geojson.Position +import org.maplibre.spatialk.geojson.Geometry + +typealias GeoJsonBoundingBox = org.maplibre.spatialk.geojson.BoundingBox + +fun BoundingBox.toGeoJsonBoundingBox(): GeoJsonBoundingBox = + GeoJsonBoundingBox( + west = min.longitude, + south = min.latitude, + east = max.longitude, + north = max.latitude + ) + +fun VisibleBounds.toStreetCompleteBoundingBox(): BoundingBox = + toBoundingBox().toBoundingBox() + +fun GeoJsonBoundingBox.toBoundingBox(): BoundingBox = + BoundingBox( + minLatitude = southwest.latitude, + minLongitude = southwest.longitude, + maxLatitude = northeast.latitude, + maxLongitude = northeast.longitude + ) + +fun ElementGeometry.toGeometry(): Geometry = when (this) { + is ElementPointGeometry -> toGeometry() + is ElementPolylinesGeometry -> toGeometry() + is ElementPolygonsGeometry -> toGeometry() +} + +fun ElementPointGeometry.toGeometry(): Point = + Point(center.toPosition()) + +fun ElementPolylinesGeometry.toGeometry(): Geometry = + if (polylines.size == 1) { + LineString(polylines.single().map { it.toPosition() }) + } else { + MultiLineString(polylines.map { polyline -> polyline.map { it.toPosition() } }) + } + +fun ElementPolygonsGeometry.toGeometry(): Geometry { + val outerRings = mutableListOf>() + val innerRings = mutableListOf>() + if (polygons.size == 1) { + outerRings.add(polygons.first()) + } else { + polygons.forEach { + if (it.isRingDefinedClockwise()) innerRings.add(it) else outerRings.add(it) + } + } + + if (outerRings.size == 1) { + return Polygon( + (outerRings + innerRings).map { ring -> ring.map { it.toPosition() } } + ) + } + + // outerRings must be sorted size ascending to correctly handle outer rings within holes + // of larger polygons. + outerRings.sortBy { it.measuredArea() } + + // we need to allocate the holes to the different outer polygons + val groupedRings = outerRings.map { outerRing -> + val rings = mutableListOf>() + rings.add(outerRing.map { it.toPosition() }) + for (innerRing in innerRings.toList()) { + if (innerRing[0].isInPolygon(outerRing)) { + innerRings.remove(innerRing) + rings.add(innerRing.map { it.toPosition() }) + } + } + rings + } + return MultiPolygon(groupedRings) +} + +fun List.toLineGeometry(): LineString? = + if (size < 2) null else LineString(map { it.toPosition() }) + +fun List>.toMultiLineGeometry() = MultiLineString( + mapNotNull { line -> if (line.size < 2) null else line.map { it.toPosition() } } +) + +fun LatLon.toGeometry(): Point = + Point(Position(longitude = longitude, latitude = latitude)) + +fun LatLon.toPosition(): Position = + Position(longitude = longitude, latitude = latitude) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMap.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMap.kt new file mode 100644 index 00000000000..b8b85d33842 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMap.kt @@ -0,0 +1,51 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.maplibre.compose.interaction.ClickEvent +import org.maplibre.compose.interaction.ClickResult +import org.maplibre.compose.interaction.MapInteractions +import org.maplibre.compose.map.CameraConstraints +import org.maplibre.compose.map.MapState +import org.maplibre.compose.map.MaplibreMap +import org.maplibre.compose.overlay.MapOverlayScope + +/** Presents the shared map. */ +@Composable +fun MainMap( + state: MapState, + modifier: Modifier = Modifier, + cameraPadding: PaddingValues = PaddingValues(0.dp), + onPan: () -> Unit = {}, + onUserCameraMove: () -> Unit = {}, + onMapClick: (ClickEvent) -> ClickResult = { ClickResult.Pass }, + onMapLongClick: (ClickEvent) -> ClickResult = { ClickResult.Pass }, + overlay: @Composable MapOverlayScope.() -> Unit = {}, +) { + MaplibreMap( + modifier = modifier, + state = state, + cameraPadding = cameraPadding, + cameraConstraints = CameraConstraints(minZoom = 0.0, maxZoom = 22.0), + interactions = MapInteractions { + camera { + pan { + onStart { + onPan() + onUserCameraMove() + } + } + zoom { onStart(onUserCameraMove) } + rotate { onStart(onUserCameraMove) } + tilt { onStart(onUserCameraMove) } + } + callbacks { + click { onUnhandled(onMapClick) } + longClick { onEvent(onMapLongClick) } + } + }, + overlay = overlay, + ) +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapCameraState.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapCameraState.kt new file mode 100644 index 00000000000..318fd8a2268 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapCameraState.kt @@ -0,0 +1,149 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.ui.util.rememberSerializable +import de.westnordost.streetcomplete.util.ktx.toLatLon +import kotlinx.serialization.Serializable +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.map.MapState +import kotlin.math.abs +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds + +@Composable +fun rememberMainMapCameraState( + mapState: MapState, + initiallyFollowing: Boolean = true, + initiallyNavigating: Boolean = false, +): MainMapCameraState { + val settings = rememberSerializable { + mutableStateOf(MapCameraSettings(initiallyFollowing, initiallyNavigating)) + } + return remember(mapState) { MainMapCameraState(mapState, settings) } +} + +/** StreetComplete's camera policy; the map itself owns its camera position and animations. */ +class MainMapCameraState internal constructor( + private val map: MapState, + settings: MutableState, +) { + private var settings by settings + + var isFollowingPosition: Boolean + get() = settings.following + set(value) { settings = settings.copy(following = value, zoomedYet = value && settings.zoomedYet) } + + val isNavigationMode: Boolean get() = settings.navigating + + /** A pan only stops following once a location is displayed, as in the legacy map. */ + fun onPan(hasLocation: Boolean) { + if (hasLocation) isFollowingPosition = false + } + + suspend fun setNavigationMode(value: Boolean, location: LatLon?, bearing: Double?) { + if (settings.navigating == value) return + settings = settings.copy(navigating = value) + if (value) { + followLocation(location, bearing) + } else { + // Leaving navigation retains the bearing; the compass resets it explicitly. + map.animateCameraPosition(map.cameraPosition.copy(tilt = 0.0), 300.milliseconds) + } + } + + suspend fun followLocation(location: LatLon?, bearing: Double?) { + if (!isFollowingPosition || location == null) return + val camera = map.cameraPosition + val zoom = if (!settings.zoomedYet && camera.zoom < 17.0) 18.0 else camera.zoom + settings = settings.copy(zoomedYet = true) + map.animateCameraPosition(camera.copy( + target = location.toPosition(), + zoom = zoom, + bearing = if (isNavigationMode) bearing ?: camera.bearing else camera.bearing, + tilt = if (isNavigationMode) 60.0 else camera.tilt, + ), 600.milliseconds) + } + + suspend fun resetCompass() { + settings = settings.copy(navigating = false) + map.animateCameraPosition(map.cameraPosition.copy(bearing = 0.0, tilt = 0.0), 300.milliseconds) + } + + suspend fun freeze() { + if (settings.frozenFollowing == null) { + settings = settings.copy(frozenFollowing = isFollowingPosition, frozenNavigation = isNavigationMode) + } + isFollowingPosition = false + setNavigationMode(false, null, null) + } + + suspend fun unfreeze(location: LatLon?, bearing: Double?) { + val following = settings.frozenFollowing ?: return + val navigating = settings.frozenNavigation + settings = settings.copy(frozenFollowing = null, frozenNavigation = false) + isFollowingPosition = following + setNavigationMode(navigating, location, bearing) + } + + fun moveTo(position: CameraPosition) { + settings = settings.copy(following = false, navigating = false, zoomedYet = false) + // MapState retains this update even before a map surface is attached. + map.setCameraPosition(position) + } + + suspend fun focus(geometry: ElementGeometry) { + val camera = map.cameraPosition + if (settings.previousFocus == null) { + settings = settings.copy(previousFocus = FocusCamera(camera.target.toLatLon(), camera.zoom)) + } + if (geometry is ElementPointGeometry) { + val difference = abs(camera.zoom - 19.0) + map.animateCameraPosition(camera.copy( + target = geometry.center.toPosition(), + zoom = if (difference > 0.5) 19.0 else camera.zoom, + ), maxOf(450, (difference * 450).roundToInt()).milliseconds) + } else { + // TODO maplibre-compose: Query the fitted camera to restore the 0.75 zoom margin, + // maximum zoom 19, 0.5 zoom threshold, and zoom-dependent duration (as for clusters). + // Requires a release containing https://github.com/maplibre/maplibre-compose/pull/1400. + map.animateCameraToBounds( + geometry.bounds.toGeoJsonBoundingBox(), camera.bearing, camera.tilt, + duration = 450.milliseconds, + ) + } + } + + fun clearFocus() { + settings = settings.copy(previousFocus = null) + } + + suspend fun endFocus() { + val previous = settings.previousFocus ?: return + clearFocus() + val camera = map.cameraPosition + // Restore the pre-focus target and zoom, keeping the user's current bearing and tilt. + map.animateCameraPosition(camera.copy(target = previous.position.toPosition(), zoom = previous.zoom), + maxOf(300, (abs(camera.zoom - previous.zoom) * 300).roundToInt()).milliseconds) + } +} + +@Serializable +internal data class MapCameraSettings( + val following: Boolean, + val navigating: Boolean, + val zoomedYet: Boolean = false, + val frozenFollowing: Boolean? = null, + val frozenNavigation: Boolean = false, + val previousFocus: FocusCamera? = null, +) + +@Serializable +internal data class FocusCamera(val position: LatLon, val zoom: Double) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapContent.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapContent.kt new file mode 100644 index 00000000000..0197a99cd9a --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapContent.kt @@ -0,0 +1,195 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.text.intl.Locale +import de.westnordost.streetcomplete.data.download.tiles.TilePos +import de.westnordost.streetcomplete.data.edithistory.Edit +import de.westnordost.streetcomplete.data.location.Location +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.overlays.Overlay +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.screens.main.ShownBottomSheet +import de.westnordost.streetcomplete.screens.main.edithistory.icon +import de.westnordost.streetcomplete.screens.main.map.layers.CurrentLocationLayers +import de.westnordost.streetcomplete.screens.main.map.layers.DownloadedAreaLayer +import de.westnordost.streetcomplete.screens.main.map.layers.FocusedGeometryLayers +import de.westnordost.streetcomplete.screens.main.map.layers.GeometryMarkersLayers +import de.westnordost.streetcomplete.screens.main.map.layers.Pin +import de.westnordost.streetcomplete.screens.main.map.layers.PinsLayers +import de.westnordost.streetcomplete.screens.main.map.layers.SelectedPinsLayer +import de.westnordost.streetcomplete.screens.main.map.layers.StyleableOverlayLabelLayer +import de.westnordost.streetcomplete.screens.main.map.layers.StyleableOverlayLayers +import de.westnordost.streetcomplete.screens.main.map.layers.StyleableOverlaySideLayer +import de.westnordost.streetcomplete.screens.main.map.layers.StyledElement +import de.westnordost.streetcomplete.screens.main.map.layers.TracksLayers +import de.westnordost.streetcomplete.screens.main.map.layers.getIcon +import de.westnordost.streetcomplete.screens.main.map.layers.toGeoJsonFeatures +import de.westnordost.streetcomplete.ui.common.quest.Marker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import org.maplibre.compose.interaction.ClickResult +import org.maplibre.compose.map.LocalMapState +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.util.MaplibreComposable +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.FeatureCollection +import org.maplibre.spatialk.geojson.Geometry +import kotlin.uuid.Uuid + +@Composable +@MaplibreComposable +internal fun MainMapContent( + location: Location?, + rotation: Float?, + isRecording: Boolean, + trackpoints: List, + oldTrackpointsLists: List>, + shownBottomSheet: ShownBottomSheet?, + shownMarkers: Collection?, + isShowingUndoHistorySidebar: Boolean, + selectedOverlay: Overlay?, + selectedEdit: Edit?, + highlightedGeometry: ElementGeometry?, + downloadedTiles: Collection, + questPins: Collection, + editHistoryPins: Collection, + styledElements: Collection, + onClickQuest: (JsonObject) -> ClickResult, + onClickEdit: (JsonObject) -> ClickResult, + onClickElement: (JsonObject) -> ClickResult, +) { + // because quests highlight additional information and history sidebar should feel clean + val showOverlay = selectedOverlay != null && shownBottomSheet !is ShownBottomSheet.OsmQuest && + shownBottomSheet !is ShownBottomSheet.OsmNoteQuest && !isShowingUndoHistorySidebar + + val selectedQuest = when (val sheet = shownBottomSheet) { + is ShownBottomSheet.OsmNoteQuest -> sheet.quest + is ShownBottomSheet.OsmQuest -> sheet.quest + else -> null + } + + val selectedOverlayElement = shownBottomSheet as? ShownBottomSheet.Overlay + val showQuestPins = !isShowingUndoHistorySidebar && shownBottomSheet == null + + val languages = listOf(Locale.current.language) + val colors = if (isSystemInDarkTheme()) MapColors.Night else MapColors.Light + + val overlayData by produceState>>(emptyList(), styledElements) { + value = withContext(Dispatchers.Default) { + styledElements.flatMap { it.toGeoJsonFeatures() } + } + } + val overlaySource = rememberGeoJsonSource( + data = GeoJsonData.Features(FeatureCollection(overlayData)), + ) + + // TODO maplibre-compose: Reuse layer IDs once the runtime includes the fix for + // https://github.com/maplibre/maplibre-native-ffi/issues/709. + val layerIdSuffix = remember(checkNotNull(LocalMapState.current).style.baseStyle) { Uuid.random().toString() } + MapStyle( + layerIdSuffix = layerIdSuffix, + colors = colors, + languages = languages, + hiddenLayers = selectedOverlay?.hidesLayers.orEmpty(), + belowRoadsContent = { + // left-and-right lines should be rendered behind the actual road + if (showOverlay) { + StyleableOverlaySideLayer( + source = overlaySource, + isBridge = false + ) + } + }, + belowRoadsOnBridgeContent = { + // left-and-right lines should be rendered behind the actual bridge road + if (showOverlay) { + StyleableOverlaySideLayer( + source = overlaySource, + isBridge = true + ) + } + }, + belowLabelsContent = { + // labels should be on top of other layers + DownloadedAreaLayer(downloadedTiles) + if (showOverlay) { + StyleableOverlayLayers( + source = overlaySource, + onClickElement = onClickElement + ) + } + TracksLayers(trackpoints, isRecording, oldTrackpointsLists) + }, + aboveLabelsContent = { + // these are always on top of everything else (including labels) + if (showOverlay) { + StyleableOverlayLabelLayer( + source = overlaySource, + icons = styledElements.mapNotNull { it.style.getIcon() }, + color = colors.text, + haloColor = colors.textOutline, + onClickElement = onClickElement + ) + } + shownMarkers?.let { markers -> + GeometryMarkersLayers(markers, haloColor = colors.textOutline) + } + (highlightedGeometry ?: shownBottomSheet?.geometry)?.let { geometry -> + FocusedGeometryLayers(geometry) + } + + location?.let { CurrentLocationLayers(location = it, rotation = rotation) } + + // normal quest pins are not shown while edit history sidebar is open + if (isShowingUndoHistorySidebar) { + PinsLayers( + pins = editHistoryPins, + onClickPin = onClickEdit + ) + } else if (showQuestPins) { + PinsLayers( + pins = questPins, + onClickPin = onClickQuest + ) + } + + val edit = selectedEdit + if (isShowingUndoHistorySidebar && edit != null) { + edit.icon?.let { icon -> SelectedPinsLayer(icon, listOf(edit.position)) } + } else if (selectedOverlayElement?.element != null) { + selectedOverlayElement.geometry?.let { geometry -> + SelectedPinsLayer(selectedOverlayElement.overlay.icon, listOf(geometry.center)) + } + } else if (selectedQuest != null) { + SelectedPinsLayer( + icon = selectedQuest.type.icon, + pinPositions = selectedQuest.markerLocations + ) + } + } + ) +} + +// need to refer to the local (font) resources platform-independently +internal val BASE_STYLE = """ + { + "version": 8, + "name": "Empty", + "metadata": {}, + "sources": {}, + "glyphs": "${ + Res.getUri("files/glyphs/Roboto Regular/0-255.pbf") + .replace("Roboto%20Regular", "{fontstack}") + .replace("Roboto Regular", "{fontstack}") + .replace("0-255", "{range}") + }", + "layers": [] + } + """.trimIndent() diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapTrackState.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapTrackState.kt new file mode 100644 index 00000000000..f61fc897afa --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapTrackState.kt @@ -0,0 +1,93 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import de.westnordost.streetcomplete.data.osmtracks.Trackpoint +import de.westnordost.streetcomplete.util.ktx.toLatLon +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.maplibre.compose.location.LocationMeasurement +import org.maplibre.spatialk.units.International + +@Composable +fun rememberMainMapTrackState(): MainMapTrackState = rememberSaveable(saver = MainMapTrackState.Saver) { + MainMapTrackState() +} + +/** Walked and recorded tracks, owned by the map screen's composition. */ +class MainMapTrackState { + private val tracks = mutableStateListOf(mutableStateListOf()) + + var isRecording by mutableStateOf(false) + private set + + val currentTrack: List get() = tracks.last().toList() + val previousTracks: List> get() = tracks.dropLast(1).map { it.toList() } + + var recordedTrack: List by mutableStateOf(emptyList()) + private set + + fun addLocation(location: LocationMeasurement) { + val accuracy = location.horizontalAccuracy?.toFloat(International.Meters) ?: 0f + if (accuracy > 20f) return + val time = location.measuredAt.toEpochMilliseconds() + val lastLocation = tracks.last().lastOrNull() + if (!isRecording && lastLocation != null && time - lastLocation.time > 60_000) { + tracks.add(mutableStateListOf()) + } + tracks.last().add(Trackpoint( + position = location.position.toLatLon(), + time = time, + accuracy = accuracy, + elevation = location.position.altitude?.toFloat() ?: 0f, + )) + } + + fun startRecording() { + isRecording = true + recordedTrack = emptyList() + tracks.add(mutableStateListOf()) + } + + fun stopRecording() { + isRecording = false + recordedTrack = currentTrack + tracks.add(mutableStateListOf()) + } + + /** Losing location clears visible tracks but does not stop recording. */ + fun clear() { + tracks.clear() + tracks.add(mutableStateListOf()) + } + + companion object { + val Saver = Saver( + save = { state -> + // Bound instance state, not the live recording attached to a new note. + var remaining = 1000 + val tracks = state.tracks.asReversed().map { track -> + track.takeLast(remaining).also { remaining -= it.size } + }.asReversed().dropWhile { it.isEmpty() }.ifEmpty { listOf(emptyList()) } + Json.encodeToString(SavedTracks(tracks, state.isRecording)) + }, + restore = { value -> + val saved = Json.decodeFromString(value) + MainMapTrackState().apply { + tracks.clear() + tracks.addAll(saved.tracks.map { it.toMutableStateList() }) + isRecording = saved.isRecording + } + }, + ) + } +} + +@Serializable +private data class SavedTracks(val tracks: List>, val isRecording: Boolean) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapViewModel.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapViewModel.kt new file mode 100644 index 00000000000..74c6d6ad7fe --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapViewModel.kt @@ -0,0 +1,114 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import de.westnordost.streetcomplete.ApplicationConstants +import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesSource +import de.westnordost.streetcomplete.data.download.tiles.TilePos +import de.westnordost.streetcomplete.data.edithistory.EditKey +import de.westnordost.streetcomplete.data.location.Location +import de.westnordost.streetcomplete.data.location.SurveyChecker +import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox +import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey +import de.westnordost.streetcomplete.data.overlays.Overlay +import de.westnordost.streetcomplete.data.overlays.SelectedOverlaySource +import de.westnordost.streetcomplete.data.quest.QuestKey +import de.westnordost.streetcomplete.screens.main.map.layers.Pin +import de.westnordost.streetcomplete.screens.main.map.layers.StyledElement +import de.westnordost.streetcomplete.screens.main.map.sources.EditHistoryPinsSource +import de.westnordost.streetcomplete.screens.main.map.sources.MapQuestPinsSource +import de.westnordost.streetcomplete.screens.main.map.sources.StyleableOverlaySource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject + +abstract class MainMapViewModel : ViewModel() { + abstract val selectedOverlay: StateFlow + + abstract fun onViewportChanged(zoom: Double, bounds: BoundingBox?) + abstract fun onLocationChanged(location: Location) + + /** Downloaded areas */ + abstract val downloadedTiles: StateFlow> + + /** Quest pins in current view */ + abstract val questPins: StateFlow> + abstract fun getQuestKey(properties: JsonObject): QuestKey? + + /** Edit history pins in current view */ + abstract val editHistoryPins: StateFlow> + abstract fun getEditKey(properties: JsonObject): EditKey? + + /** Styled elements (of overlay) in current view */ + abstract val styleableElements: StateFlow> + abstract fun getElementKey(properties: JsonObject): ElementKey? +} + +class MainMapViewModelImpl( + private val downloadedTilesSource: DownloadedTilesSource, + private val mapQuestPinsSource: MapQuestPinsSource, + private val editHistoryPinsSource: EditHistoryPinsSource, + private val styleableOverlaySource: StyleableOverlaySource, + private val selectedOverlaySource: SelectedOverlaySource, + private val surveyChecker: SurveyChecker, +) : MainMapViewModel() { + + override val selectedOverlay = callbackFlow { + val listener = object : SelectedOverlaySource.Listener { + override fun onSelectedOverlayChanged() { trySend(selectedOverlaySource.selectedOverlay) } + } + selectedOverlaySource.addListener(listener) + trySend(selectedOverlaySource.selectedOverlay) + awaitClose { selectedOverlaySource.removeListener(listener) } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), null) + + override fun onLocationChanged(location: Location) { + surveyChecker.addRecentLocation(location) + } + + override fun onViewportChanged(zoom: Double, bounds: BoundingBox?) { + mapQuestPinsSource.onMapMoved(zoom, bounds) + styleableOverlaySource.onMapMoved(zoom, bounds) + } + + @OptIn(ExperimentalCoroutinesApi::class) + override val downloadedTiles = callbackFlow { + val listener = object : DownloadedTilesSource.Listener { + override fun onUpdated() { trySend(Unit) } + } + downloadedTilesSource.addListener(listener) + trySend(Unit) + awaitClose { + downloadedTilesSource.removeListener(listener) + } + }.buffer(Channel.CONFLATED).mapLatest { + withContext(Dispatchers.IO) { + downloadedTilesSource.getAll(ApplicationConstants.DELETE_OLD_DATA_AFTER) + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), emptyList()) + + override val questPins = mapQuestPinsSource.pins + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), emptyList()) + + override fun getQuestKey(properties: JsonObject) = mapQuestPinsSource.getQuestKey(properties) + + override val editHistoryPins = editHistoryPinsSource.pins + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), emptyList()) + + override fun getEditKey(properties: JsonObject) = editHistoryPinsSource.getEditKey(properties) + + override val styleableElements = styleableOverlaySource.styledElements + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), emptyList()) + + override fun getElementKey(properties: JsonObject) = styleableOverlaySource.getElementKey(properties) +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapColors.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapColors.kt new file mode 100644 index 00000000000..bf3a81a0e75 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapColors.kt @@ -0,0 +1,100 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +/** Map colors schema for [MapStyle] */ +@Immutable +data class MapColors( + val earth: Color, + val water: Color, + val waterShore: Color, + val green: Color, + val forest: Color, + val town: Color, + val building: Color, + val buildingOutline: Color, + val barrier: Color, + val pointBarrier: Color, + val adminBoundary: Color, + val railway: Color, + val aeroway: Color, + val path: Color, + val steps: Color, + val road: Color, + val roadOutline: Color, + val pedestrian: Color, + val motorway: Color, + val motorwayOutline: Color, + val text: Color, + val textOutline: Color, + val textWater: Color, + val textWaterOutline: Color, + val privateOverlay: Color, + val hillshadeLight: Color, + val hillshadeShadow: Color, +) { + companion object +} + +val MapColors.Companion.Light get() = mapColorsLight +private val mapColorsLight = MapColors( + earth = Color(0xfff3eacc), + water = Color(0xff6688dd), + waterShore = Color(0xffaabbee), + green = Color(0xffc6ddaa), + forest = Color(0xffa8c884), + town = Color(0xfff3dacd), + building = Color(204, 214, 238), + buildingOutline = Color(185, 195, 217), + barrier = Color(185, 195, 217), + pointBarrier = Color(0xff888888), + adminBoundary = Color(0xffee3399), + railway = Color(0xff9999aa), + aeroway = Color(0xffffffff), + path = Color(0xffccaa99), + steps = Color(0xfff6eee6), + road = Color(0xffffffff), + roadOutline = Color(0xffccaa99), + pedestrian = Color(0xfff6eee6), + motorway = Color(0xffffaa88), + motorwayOutline = Color(0xffaa8888), + text = Color(0xff112244), + textOutline = Color(0xffffffff), + textWater = Color(0xffffffff), + textWaterOutline = Color(0xff334499), + privateOverlay = Color(0xfff3dacd), + hillshadeLight = Color.hsl(220f, 1f, 0.95f), + hillshadeShadow = Color.hsl(18f, 1f, 0.4f), +) + +val MapColors.Companion.Night get() = mapColorsNight +private val mapColorsNight = MapColors( + earth = Color(0xff2e2e48), + water = Color(0xff000022), + waterShore = Color(0xff222288), + green = Color(0xff363054), + forest = Color(0xff403962), + town = Color(0xff3d364e), + building = Color(41, 92, 92, 204), + buildingOutline = Color(31, 82, 82, 204), + barrier = Color(31, 82, 82, 204), + pointBarrier = Color(0xff9999ff), + adminBoundary = Color(0xffee7722), + railway = Color(0xff9966cc), + aeroway = Color(0xff555599), + path = Color(0xff554477), + steps = Color(0xff554e7e), + road = Color(0xff555599), + roadOutline = Color(0xff554477), + pedestrian = Color(0xff554e7e), + motorway = Color(0xff666699), + motorwayOutline = Color(0xff9999ff), + text = Color(0xffccccff), + textOutline = Color(0xff2e2e48), + textWater = Color(0xff2e2e48), + textWaterOutline = Color(0xffccccff), + privateOverlay = Color(0xff3d364e), + hillshadeLight = Color.hsl(240f, 0.3f, 0.5f), + hillshadeShadow = Color.hsl(240f, 0.8f, 0.0f), +) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapIconImage.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapIconImage.kt new file mode 100644 index 00000000000..95ee6464e71 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapIconImage.kt @@ -0,0 +1,45 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.ktx.id +import de.westnordost.streetcomplete.ui.util.ColorFilterPainter +import de.westnordost.streetcomplete.ui.util.WithHaloPainter +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource +import org.maplibre.compose.expressions.ast.Expression +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.value.ImageValue + +/** Icons are at most 48 dp plus their halo; only monochrome preset icons are tinted. */ +@Composable +internal fun mapIconImage( + icon: DrawableResource, + color: Color, + haloColor: Color +): Expression { + val painter = painterResource(icon) + val density = LocalDensity.current + val haloWidth = 2.5.dp + val haloPainter = remember(icon, painter, density, color, haloColor) { + val tintedPainter = if (icon.id?.startsWith("preset_") == true) { + ColorFilterPainter(painter, ColorFilter.tint(color)) + } else painter + WithHaloPainter(tintedPainter, density, haloWidth, haloColor) + } + val size = with(density) { + DpSize( + painter.intrinsicSize.width.toDp().coerceAtMost(48.dp) + haloWidth * 2, + painter.intrinsicSize.height.toDp().coerceAtMost(48.dp) + haloWidth * 2, + ) + } + return image( + haloPainter, + size = size, + ) +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapStyle.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapStyle.kt new file mode 100644 index 00000000000..00e369f983b --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MapStyle.kt @@ -0,0 +1,773 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.map_attribution_osm +import de.westnordost.streetcomplete.resources.map_oneway_arrow +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.maplibre.compose.expressions.ast.Expression +import org.maplibre.compose.expressions.dsl.Feature +import org.maplibre.compose.expressions.dsl.all +import org.maplibre.compose.expressions.dsl.asNumber +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.convertToString +import org.maplibre.compose.expressions.dsl.feature +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.dsl.not +import org.maplibre.compose.expressions.value.BooleanValue +import org.maplibre.compose.expressions.value.IconRotationAlignment +import org.maplibre.compose.expressions.value.LineCap +import org.maplibre.compose.expressions.value.LineJoin +import org.maplibre.compose.expressions.value.SymbolPlacement +import org.maplibre.compose.layers.BackgroundLayer +import org.maplibre.compose.layers.CircleLayer +import org.maplibre.compose.layers.FillExtrusionLayer +import org.maplibre.compose.layers.FillLayer +import org.maplibre.compose.layers.LineLayer +import org.maplibre.compose.layers.SymbolLayer +import org.maplibre.compose.sources.TileSetOptions +import org.maplibre.compose.sources.VectorSource +import org.maplibre.compose.sources.rememberVectorTileSource +import org.maplibre.compose.util.DpPadding +import org.maplibre.compose.util.MaplibreComposable +import kotlin.math.max + +/** + * StreetComplete background map style using the tile schema from JawgMaps as defined in + * https://www.jawg.io/docs/apidocs/maps/streets-v2/ + * + * It allows to inject more content at the following points: + * - [belowRoadsContent]: directly below roads and above landscape, buildings + * - [belowRoadsOnBridgeContent]: same as above but below road bridges + * - [belowLabelsContent]: above everything but labels + * - [aboveLabelsContent]: above everything + * */ +@Composable @MaplibreComposable +fun MapStyle( + colors: MapColors, + languages: List, + layerIdSuffix: String, + hiddenLayers: Collection = emptyList(), + belowRoadsContent: @Composable @MaplibreComposable () -> Unit = {}, + belowRoadsOnBridgeContent: @Composable @MaplibreComposable () -> Unit = {}, + belowLabelsContent: @Composable @MaplibreComposable () -> Unit = {}, + aboveLabelsContent: @Composable @MaplibreComposable () -> Unit = {}, +) { + // Keep the tile URL and max zoom in sync with files/map-download-style.json. + val accessToken = "mL9X4SwxfsAGfojvGiion9hPKuGLKxPbogLyMbtakA2gJ3X88gcVlTSQ7OD6OfbZ" + val osmAttribution = stringResource(Res.string.map_attribution_osm) + val attributionHtml = remember(osmAttribution) { + "$osmAttribution " + + "© JawgMaps" + } + val source = rememberVectorTileSource( + tiles = listOf("https://tile.jawg.io/streets-v2+hillshade-v1/{z}/{x}/{y}.pbf?access-token=$accessToken"), + options = TileSetOptions(maxZoom = 16, attributionHtml = attributionHtml) + ) + + val paths = remember(colors) { + RoadType( + id = "paths", + minZoom = 15f, + filters = Feature.inClass("path"), + color = colors.path, + colorOutline = colors.path, + widthStops = listOf(14.0 to 0.5.dp, 16.0 to 1.0.dp, 24.0 to 256.0.dp) // ~1m + ) + } + val pedestrian = remember(colors) { + RoadType( + id = "pedestrian", + minZoom = 14f, + filters = all(Feature.inClass("street_limited"), Feature.inType("pedestrian")), + color = colors.pedestrian, + colorOutline = colors.roadOutline, + widthStops = listOf(13 to 1.5.dp, 16 to 4.dp, 24 to 1024.dp), // ~4m + ) + } + val serviceRoads = remember(colors) { + RoadType( + id = "roads-service", + minZoom = 14f, + filters = Feature.inClass("service", "driveway"), + color = colors.road, + colorOutline = colors.roadOutline, + widthStops = listOf(13 to 0.5.dp, 16 to 3.dp, 24 to 768.dp), // ~3m + ) + } + val minorRoads = remember(colors) { + RoadType( + id = "roads-minor", + minZoom = 12f, + filters = all(Feature.inClass("street", "street_limited"), !Feature.inType("pedestrian")), + color = colors.road, + colorOutline = colors.roadOutline, + widthStops = listOf(13 to 1.5.dp, 16 to 4.dp, 24 to 1024.dp), // ~4m + ) + } + val majorRoads = remember(colors) { + RoadType( + id = "roads-major", + minZoom = 5f, + filters = Feature.inClass("main"), + color = colors.road, + colorOutline = colors.roadOutline, + widthStops = listOf(9 to 1.dp, 16 to 6.dp, 24 to 1536.dp), // ~6m + ) + } + val motorways = remember(colors) { + RoadType( + id = "motorways", + minZoom = 5f, + filters = Feature.inClass("motorway"), + color = colors.motorway, + colorOutline = colors.motorwayOutline, + widthStops = listOf(8 to 1.dp, 16 to 8.dp, 24 to 2048.dp), // ~8m + ) + } + val motorwayLinks = remember(colors) { + RoadType( + id = "motorway-links", + minZoom = 5f, + filters = Feature.inClass("motorway_link"), + color = colors.motorway, + colorOutline = colors.motorwayOutline, + widthStops = listOf(11 to 1.dp, 16 to 4.dp, 24 to 1024.dp), // ~4m + ) + } + val roads = listOf(pedestrian, serviceRoads, minorRoads, majorRoads, motorways, motorwayLinks) + + LandLayers(source, colors, layerIdSuffix) + HillshadeLayers(source, colors) + WaterLayers(source, colors, Structure.None) + AerowaysLayer(source, colors) + BuildingLayers(source, colors) + + RoadLayers(source, colors, roads, paths, serviceRoads, Structure.Tunnel, layerIdSuffix) + + PedestrianAreaLayers(source, colors, Structure.None) + + belowRoadsContent() + + RoadLayers(source, colors, roads, paths, serviceRoads, Structure.None, layerIdSuffix) + RailwayLayer(source, colors, Structure.None) + + BarriersLayers(source, colors) + + BridgeAreasLayers(source, colors) + + WaterLayers(source, colors, Structure.Bridge) + PedestrianAreaLayers(source, colors, Structure.Bridge) + + belowRoadsOnBridgeContent() + + RoadLayers(source, colors, roads, paths, serviceRoads, Structure.Bridge, layerIdSuffix) + RailwayLayer(source, colors, Structure.Bridge) + + OnewayArrowsLayer(source, colors) + BoundaryLayer(source, colors) + + belowLabelsContent() + + LabelLayers(source, colors, languages, hiddenLayers) + + // I don't know, kind of does not look good due to missing extrusion outline. + //BuildingExtrudeLayer(source, colors) + + aboveLabelsContent() +} + +@Composable @MaplibreComposable +private fun LandLayers(source: VectorSource, colors: MapColors, layerIdSuffix: String) { + BackgroundLayer( + id = "background-$layerIdSuffix", + color = const(colors.earth) + ) + FillLayer( + id = "landuse-town", + source = source, + sourceLayer = "landuse", + minZoom = 11f, + filter = !feature.inClass("pitch", "park", "grass", "cemetery", "wood", "scrub", "national_park"), + opacity = fadeInAtZoom(11f), + color = const(colors.town), + ) + FillLayer( + id = "landuse-green", + source = source, + sourceLayer = "landuse", + minZoom = 5f, + filter = feature.inClass("pitch", "park", "grass", "cemetery"), + opacity = fadeInAtZoom(5f), + color = const(colors.green), + ) + FillLayer( + id = "landuse-forest", + source = source, + sourceLayer = "landuse", + minZoom = 5f, + filter = feature.inClass("wood", "scrub"), + opacity = fadeInAtZoom(5f), + color = const(colors.forest), + ) + LineLayer( + id = "landuse-pitch-outline", + source = source, + sourceLayer = "landuse", + minZoom = 15f, + filter = feature.inClass("pitch"), + opacity = fadeInAtZoom(15f), + color = const(colors.earth), + width = byZoom(16 to 1.dp, 24 to 128.dp), + offset = byZoom(16 to 0.5.dp, 24 to 64.dp) + ) +} + +@Composable @MaplibreComposable +private fun HillshadeLayers(source: VectorSource, colors: MapColors) { + for (i in 1..2) { + FillLayer( + id = "hillshade-highlight-$i", + source = source, + sourceLayer = "hillshade", + maxZoom = 16f, + filter = feature.has("highlight", i), + opacity = fadeOutAtZoom(12f, range = 4f, startOpacity = 0.12f), + color = const(colors.hillshadeLight), + antialias = const(false), + ) + } + for (i in 1..4) { + FillLayer( + id = "hillshade-shadow-$i", + source = source, + sourceLayer = "hillshade", + maxZoom = 16f, + filter = feature.has("shadow", i), + opacity = fadeOutAtZoom(12f, range = 4f, startOpacity = 0.05f), + color = const(colors.hillshadeShadow), + antialias = const(false), + ) + } +} + +@Composable @MaplibreComposable +private fun WaterLayers(source: VectorSource, colors: MapColors, structure: Structure) { + FillLayer( + id = listOfNotNull("water-areas", structure.id).joinToString("-"), + source = source, + sourceLayer = "water", + filter = feature.isStructure(structure), + color = const(colors.water) + ) + // drawing these lines on bridges looks weird + if (structure == Structure.None) { + LineLayer( + id = listOfNotNull("water-shore-lines", structure.id).joinToString("-"), + source = source, + sourceLayer = "water", + minZoom = 15f, + filter = feature.isStructure(structure), + opacity = fadeInAtZoom(15f, range = 3f), + color = const(colors.waterShore), + width = byZoom(15 to 1.dp, 18 to 4.dp, 24 to 256.dp), + offset = byZoom(15 to 1.dp, 18 to 4.dp, 24 to 256.dp), + miterLimit = const(6) + ) + } + LineLayer( + id = listOfNotNull("rivers", structure.id).joinToString("-"), + source = source, + sourceLayer = "waterway", + minZoom = 10f, + filter = all( + feature.inClass("river", "canal"), + feature.isStructure(structure), + feature.isLines() + ), + color = const(colors.water), + width = byZoom(10 to 1.dp, 16 to 3.dp, 24 to 768.dp), + cap = const(LineCap.Round), + join = const(LineJoin.Round) + ) + LineLayer( + id = listOfNotNull("streams", structure.id).joinToString("-"), + source = source, + sourceLayer = "waterway", + minZoom = 10f, + filter = all( + feature.inClass("stream", "ditch", "drain"), + feature.isStructure(structure), + feature.isLines() + ), + color = const(colors.water), + width = byZoom(16 to 1.dp, 24 to 256.dp), + cap = const(LineCap.Round), + join = const(LineJoin.Round) + ) +} + +@Composable @MaplibreComposable +private fun AerowaysLayer(source: VectorSource, colors: MapColors) { + LineLayer( + id = "aeroways", + source = source, + sourceLayer = "aeroway", + filter = feature.isLines(), + color = const(colors.aeroway), + width = byZoom(10 to 1.dp, 24 to 8192.dp), + join = const(LineJoin.Round) + ) +} + +@Composable @MaplibreComposable +private fun BuildingLayers(source: VectorSource, colors: MapColors) { + FillLayer( + id = "buildings", + source = source, + sourceLayer = "building", + minZoom = 15f, + opacity = fadeInAtZoom(15f), + color = const(colors.building) + ) + LineLayer( + id = "buildings-outline", + source = source, + sourceLayer = "building", + minZoom = 15f, + opacity = fadeInAtZoom(15.5f, range = 0.5f), + color = const(colors.buildingOutline), + width = byZoom(16 to 1.dp, 24 to 128.dp) + ) +} + +@Composable @MaplibreComposable +private fun PedestrianAreaLayers(source: VectorSource, colors: MapColors, structure: Structure) { + val filter = all( + feature.inClass("path", "street_limited"), + feature.isArea(), + feature.isStructure(structure), + ) + + LineLayer( + id = listOfNotNull("pedestrian-areas-casing", structure.id).joinToString("-"), + source = source, + sourceLayer = "road", + minZoom = 16f, + filter = filter, + opacity = fadeInAtZoom(16f), + color = const(colors.roadOutline), + width = byZoom(16 to 1.dp, 24 to 128.dp), + offset = byZoom(16 to -0.5.dp, 24 to -64.dp), + ) + FillLayer( + id = listOfNotNull("pedestrian-areas", structure.id).joinToString("-"), + source = source, + sourceLayer = "road", + minZoom = 15f, + filter = filter, + opacity = fadeInAtZoom(15f), + color = const(colors.pedestrian) + ) +} + +@Composable @MaplibreComposable +private fun RoadLayers( + source: VectorSource, + colors: MapColors, + roads: List, + paths: RoadType, + serviceRoads: RoadType, + structure: Structure, + layerIdSuffix: String, +) { + // for roads, first draw the casing (= outline) of all roads + for (road in roads) { + RoadCasingLayer(road, source, structure) + } + // , then draw the road color... + RoadLayer(paths, source, structure, layerIdSuffix) + StepsOverlayLayer(source, colors, structure) + for (road in roads) { + RoadLayer(road, source, structure, layerIdSuffix) + } + RoadPrivateOverlayLayer(paths, source, colors, structure) + RoadPrivateOverlayLayer(serviceRoads, source, colors, structure) +} + +@Composable @MaplibreComposable +private fun BarriersLayers(source: VectorSource, colors: MapColors) { + LineLayer( + id = "barriers-large", + source = source, + sourceLayer = "structure", + minZoom = 15f, + filter = feature.inType("city_wall", "dam", "cliff"), + opacity = fadeInAtZoom(15f), + color = const(colors.buildingOutline), + width = byZoom(16 to 4.dp, 24 to 768.dp), + ) + LineLayer( + id = "barriers-wall", + source = source, + sourceLayer = "structure", + minZoom = 15f, + filter = all( + feature.inClass("fence"), + !feature.inType("city_wall"), + ), + opacity = fadeInAtZoom(15f), + color = const(colors.buildingOutline), + width = byZoom(16 to 1.dp, 24 to 256.dp), + ) + LineLayer( + id = "barriers-hedges", + source = source, + sourceLayer = "structure", + minZoom = 15f, + filter = feature.inClass("hedge"), + opacity = fadeInAtZoom(15f), + color = const(colors.forest), + width = byZoom(16 to 1.dp, 24 to 512.dp), + ) + CircleLayer( + id = "point-barriers", + source = source, + sourceLayer = "structure", + minZoom = 17f, + filter = feature.isPoint(), + color = const(colors.pointBarrier), + radius = byZoom(17 to 2.dp, 24 to 256.dp), + ) +} + +@Composable @MaplibreComposable +private fun BridgeAreasLayers(source: VectorSource, colors: MapColors) { + FillLayer( + id = "bridge-areas", + source = source, + sourceLayer = "structure", + filter = all(feature.isArea(), feature.inClass("bridge")), + opacity = const(0.8f), + color = const(colors.building) + ) + LineLayer( + id = "bridge-lines", + source = source, + sourceLayer = "structure", + filter = all(feature.isLines(), feature.inClass("bridge")), + opacity = const(0.8f), + color = const(colors.building), + width = byZoom(16 to 4.dp, 24 to 512.dp), + ) +} + +@Composable @MaplibreComposable +private fun OnewayArrowsLayer(source: VectorSource, colors: MapColors) { + SymbolLayer( + id = "oneway-arrows", + source = source, + sourceLayer = "road", + minZoom = 17f, + filter = all(feature.isLines(), feature.has("oneway", true)), + placement = const(SymbolPlacement.LineCenter), + spacing = byZoom(17 to 200.dp, 24 to 25600.dp), + iconImage = image(painterResource(Res.drawable.map_oneway_arrow)), + iconOpacity = const(0.5f), + iconSize = byZoom(17 to 0.25f, 24 to 16.0f), + iconPadding = const(DpPadding(5.dp, 5.dp, 5.dp, 5.dp)), + iconRotate = const(90), + iconRotationAlignment = const(IconRotationAlignment.Map) + ) +} + +@Composable @MaplibreComposable +private fun BoundaryLayer(source: VectorSource, colors: MapColors) { + LineLayer( + id = "boundaries", + source = source, + sourceLayer = "admin", + filter = all( + !feature.has("maritime", true), + feature.has("admin_level", 2), + ), + color = const(colors.adminBoundary), + width = const(1.dp), + dasharray = const(listOf(1, 2)) + ) +} + +@Composable @MaplibreComposable +private fun LabelLayers( + source: VectorSource, + colors: MapColors, + languages: List, + hiddenLayers: Collection, +) { + val localizedName = feature.localizedName(languages) + val haloWidth = const(2.5.dp) + val textFont = const(listOf("Roboto Regular")) + val textSize = byZoom(1.0 to 13.sp, 24 to 64.sp) + val textPadding = const(12.dp) + + SymbolLayer( + id = "labels-country", + source = source, + sourceLayer = "place_label", + filter = feature.inClass("country"), + sortKey = feature["scalerank"].asNumber(), + textField = localizedName, + textColor = const(colors.text), + textHaloColor = const(colors.textOutline), + textHaloWidth = haloWidth, + textFont = textFont, + textSize = textSize, + textPadding = textPadding, + ) + SymbolLayer( + id = "labels-localities", + source = source, + sourceLayer = "place_label", + filter = feature.inClass("locality"), + sortKey = feature["scalerank"].asNumber(), + textField = localizedName, + textColor = const(colors.text), + textHaloColor = const(colors.textOutline), + textHaloWidth = haloWidth, + textFont = textFont, + textSize = textSize, + textPadding = textPadding, + ) + SymbolLayer( + id = "labels-housenumbers", + visible = "labels-housenumbers" !in hiddenLayers, + source = source, + sourceLayer = "housenum_label", + minZoom = 18f, + sortKey = feature["scalerank"].asNumber(), + textField = feature["house_num"].convertToString(), + textColor = const(colors.text), + textHaloColor = const(colors.textOutline), + textHaloWidth = haloWidth, + textFont = textFont, + textSize = textSize, + textPadding = textPadding, + ) + SymbolLayer( + id = "labels-road", + source = source, + sourceLayer = "road", + minZoom = 14f, + filter = feature.isLines(), + placement = const(SymbolPlacement.LineCenter), + sortKey = feature["scalerank"].asNumber(), + textField = localizedName, + textColor = const(colors.text), + textHaloColor = const(colors.textOutline), + textHaloWidth = haloWidth, + textFont = textFont, + textSize = textSize, + textPadding = textPadding, + ) + SymbolLayer( + id = "labels-rivers", + source = source, + sourceLayer = "waterway", + minZoom = 14f, + filter = all(!feature.isTunnel(), feature.inClass("river", "canal")), + placement = const(SymbolPlacement.LineCenter), + sortKey = feature["scalerank"].asNumber(), + textField = localizedName, + textColor = const(colors.textWater), + textHaloColor = const(colors.textWaterOutline), + textHaloWidth = haloWidth, + textFont = textFont, + textSize = textSize, + textPadding = textPadding, + ) + SymbolLayer( + id = "labels-streams", + source = source, + sourceLayer = "waterway", + minZoom = 16f, + filter = all(!feature.isTunnel(), feature.inClass("stream", "ditch", "drain")), + placement = const(SymbolPlacement.LineCenter), + sortKey = feature["scalerank"].asNumber(), + textField = localizedName, + textColor = const(colors.textWater), + textHaloColor = const(colors.textWaterOutline), + textHaloWidth = haloWidth, + textFont = textFont, + textSize = textSize, + textPadding = textPadding, + ) +} + +@Composable @MaplibreComposable +private fun BuildingExtrudeLayer(source: VectorSource, colors: MapColors) { + FillExtrusionLayer( + id = "buildings-extrude", + source = source, + sourceLayer = "building", + minZoom = 15f, + maxZoom = 19f, + filter = feature.has("extrude", true), + opacity = byZoom(15 to 0f, 16 to 0.8f, 18 to 0.8f, 19 to 0f), + color = const(colors.building), + base = feature["min_height"].asNumber(), + height = feature["height"].asNumber() + ) +} + +@Composable @MaplibreComposable +private fun RoadLayer( + road: RoadType, + source: VectorSource, + structure: Structure, + layerIdSuffix: String, +) { + LineLayer( + id = listOfNotNull(road.id, structure.id, layerIdSuffix).joinToString("-"), + source = source, + sourceLayer = "road", + minZoom = road.minZoom, + filter = all( + feature.isLines(), + feature.isStructure(structure), + road.filters, + ), + opacity = when { + structure == Structure.Tunnel -> const(0.25f) + road.minZoom != 0f -> fadeInAtZoom(road.minZoom) + else -> const(1f) + }, + color = const(road.color), + width = byZoom(*road.widthStops.toTypedArray()), + cap = const(LineCap.Round), + join = const(LineJoin.Round), + ) +} + +@Composable @MaplibreComposable +private fun RoadCasingLayer(road: RoadType, source: VectorSource, structure: Structure) { + LineLayer( + id = listOfNotNull(road.id, structure.id, "casing").joinToString("-"), + source = source, + sourceLayer = "road", + minZoom = max(15.5f, road.minZoom), + filter = all( + feature.isLines(), + feature.isStructure(structure), + road.filters + ), + opacity = fadeInAtZoom(15f), + color = const(road.colorOutline), + dasharray = if (structure == Structure.Tunnel) const(listOf(4, 4)) else null, + width = byZoom(16 to 1.dp, 24 to 128.dp), + gapWidth = byZoom(*road.widthStops.toTypedArray()), + // cap must not be round for bridges so that the casing is not drawn on top of normal roads + cap = if (structure == Structure.None) const(LineCap.Round) else const(LineCap.Butt), + join = const(LineJoin.Round) + ) +} + +@Composable @MaplibreComposable +private fun RoadPrivateOverlayLayer(road: RoadType, source: VectorSource, colors: MapColors, structure: Structure) { + LineLayer( + id = listOfNotNull(road.id, structure.id, "private").joinToString("-"), + source = source, + sourceLayer = "road", + minZoom = road.minZoom, + filter = all( + feature.isLines(), + feature.hasAny("access", listOf("no", "private", "destination", "customers", "delivery", "agricultural", "forestry", "emergency")), + feature.isStructure(structure), + road.filters + ), + color = const(colors.privateOverlay), + dasharray = const(listOf(1, 2)), + width = byZoom(*road.widthStops.map { (z, w) -> z to (w * 0.5f) }.toTypedArray()), + cap = const(LineCap.Round), + join = const(LineJoin.Round), + ) +} + +@Composable @MaplibreComposable +private fun RailwayLayer(source: VectorSource, colors: MapColors, structure: Structure) { + LineLayer( + id = listOfNotNull("railways", structure.id).joinToString("-"), + source = source, + sourceLayer = "road", + minZoom = 12f, + filter = all( + feature.inClass("major_rail", "minor_rail"), + feature.isLines(), + feature.isStructure(structure) + ), + opacity = fadeInAtZoom(12f), + color = const(colors.railway), + // at zoom 17, the line spits up into two lines, to mimic the two tracks of a railway + width = byZoom(12 to 0.75.dp, 13 to 2.dp, 16.999 to 4.dp, 17.0 to 2.dp, 24.0 to 128.dp), + gapWidth = byZoom(12 to 0.dp, 17 to 0.dp, 24 to 256.dp), + join = const(LineJoin.Round) + ) +} + +@Composable @MaplibreComposable +private fun StepsOverlayLayer(source: VectorSource, colors: MapColors, structure: Structure) { + LineLayer( + id = listOfNotNull("steps", structure.id).joinToString("-"), + source = source, + sourceLayer = "road", + minZoom = 15f, + filter = all( + feature.inClass("path"), + feature.inType("steps"), + feature.isLines(), + feature.isStructure(structure) + ), + opacity = if (structure == Structure.Tunnel) const(0.25f) else const(1f), + color = const(colors.pedestrian), + dasharray = const(listOf(0.6f, 0.4f)), + // 75% of path width + width = byZoom(14 to 0.375.dp, 16 to 0.75.dp, 24 to 192.dp) + ) +} + +@Immutable +private data class RoadType( + val id: String, + val minZoom: Float = 0.0f, + val filters: Expression, + val color: Color, + val colorOutline: Color, + val widthStops: List>, +) + +private enum class Structure(val id: String?) { Bridge("bridge"), Tunnel("tunnel"), None(null) } + +private fun Feature.isStructure(structure: Structure) = when (structure) { + Structure.Bridge -> isBridge() + Structure.Tunnel -> isTunnel() + Structure.None -> isOnGround() +} +private fun Feature.isBridge() = has("structure", "bridge") +private fun Feature.isTunnel() = has("structure", "tunnel") +private fun Feature.isOnGround() = !hasAny("structure", listOf("bridge", "tunnel")) + +private fun Feature.inClass(vararg values: String) = hasAny("class", values.toList()) +private fun Feature.inType(vararg values: String) = hasAny("type", values.toList()) + +private fun Feature.localizedName(languages: List) = localizedName( + languages = languages, + nameKey = "name", + localizedNameKey = { "name_$it" }, + extraNameKeys = listOf("name_ltn") +) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/PinPainter.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/PinPainter.kt new file mode 100644 index 00000000000..8f194af9d6a --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/PinPainter.kt @@ -0,0 +1,47 @@ +package de.westnordost.streetcomplete.screens.main.map + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.inset +import androidx.compose.ui.graphics.painter.Painter +import de.westnordost.streetcomplete.resources.* +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource + +/** Draws a (quest) pin */ +class PinPainter( + private val iconPainter: Painter, + private val pinPainter: Painter, + private val pinShadowPainter: Painter, +) : Painter() { + override val intrinsicSize: Size + get() = pinShadowPainter.intrinsicSize + + override fun DrawScope.onDraw() { + val sX = size.width / 71f + val sY = size.height / 71f + + with(pinShadowPainter) { draw(size) } + inset(left = 14f * sX, top = 5f * sY, right = 5f * sX, bottom = 0f * sY) { + with(pinPainter) { draw(size) } + inset(left = 2f * sX, top = 2f * sY, right = 2f * sX, bottom = 16f * sY) { + with(iconPainter) { draw(size) } + } + } + } +} + +@Composable +fun pinPainter(iconPainter: Painter): Painter { + val pinPainter = painterResource(Res.drawable.pin) + val pinShadowPainter = painterResource(Res.drawable.pin_shadow) + return remember(iconPainter, pinPainter, pinShadowPainter) { + PinPainter( + iconPainter = iconPainter, + pinPainter = pinPainter, + pinShadowPainter = pinShadowPainter + ) + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/CurrentLocationLayers.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/CurrentLocationLayers.kt new file mode 100644 index 00000000000..b82d7e09a06 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/CurrentLocationLayers.kt @@ -0,0 +1,105 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.animation.core.Spring.StiffnessLow +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.data.location.Location +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.location_nyan +import de.westnordost.streetcomplete.resources.location_shadow +import de.westnordost.streetcomplete.resources.location_view_direction +import de.westnordost.streetcomplete.screens.main.map.animateLatLonAsState +import de.westnordost.streetcomplete.screens.main.map.inMeters +import de.westnordost.streetcomplete.screens.main.map.toGeometry +import de.westnordost.streetcomplete.ui.theme.Location +import de.westnordost.streetcomplete.util.ktx.isApril1st +import org.jetbrains.compose.resources.painterResource +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.value.CirclePitchAlignment +import org.maplibre.compose.expressions.value.IconPitchAlignment +import org.maplibre.compose.layers.CircleLayer +import org.maplibre.compose.layers.SymbolLayer +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.util.MaplibreComposable + +/** Displays the location + direction + accuracy marker on the map */ +@Composable @MaplibreComposable +fun CurrentLocationLayers( + location: Location, + rotation: Float? +) { + // Use the same spring as TracksLayers so the marker and track endpoint move together. + val animatedPosition by animateLatLonAsState(targetValue = location.position) + val animatedAccuracy by animateFloatAsState( + targetValue = location.accuracy, + animationSpec = spring(stiffness = StiffnessLow), + ) + + // let's not check for the date on every recomposition :-) + val isApril1st = remember { isApril1st() } + + val source = rememberGeoJsonSource(GeoJsonData.Features(animatedPosition.toGeometry())) + + CircleLayer( + id = "accuracy", + source = source, + opacity = const(0.15f), + color = const(Color.Location), + radius = inMeters( + width = animatedAccuracy, + latitude = animatedPosition.latitude + ), + strokeOpacity = const(0.5f), + strokeColor = const(Color.Location), + strokeWidth = const(1.dp), + pitchAlignment = const(CirclePitchAlignment.Map), + ) + if (rotation != null) { + SymbolLayer( + id = "direction", + source = source, + iconImage = image(painterResource(Res.drawable.location_view_direction)), + iconAllowOverlap = const(true), + iconIgnorePlacement = const(true), + iconRotate = const(rotation), + iconPitchAlignment = const(IconPitchAlignment.Map), + ) + } + SymbolLayer( + id = "location-shadow", + source = source, + iconImage = image(painterResource(Res.drawable.location_shadow)), + iconAllowOverlap = const(true), + iconIgnorePlacement = const(true), + iconPitchAlignment = const(IconPitchAlignment.Map), + ) + if (!isApril1st) { + CircleLayer( + id = "location", + source = source, + color = const(Color.Location), + radius = const(8.dp), + strokeColor = const(Color.White), + strokeWidth = const(2.dp), + pitchAlignment = const(CirclePitchAlignment.Map) + ) + } else { + SymbolLayer( + id = "location-nyan", + source = source, + iconImage = image(painterResource(Res.drawable.location_nyan)), + iconSize = const(2f), + iconAllowOverlap = const(true), + iconIgnorePlacement = const(true), + iconPitchAlignment = const(IconPitchAlignment.Viewport), + ) + } +} + diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/DownloadedAreaLayer.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/DownloadedAreaLayer.kt new file mode 100644 index 00000000000..a1137c97be7 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/DownloadedAreaLayer.kt @@ -0,0 +1,56 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import de.westnordost.streetcomplete.ApplicationConstants +import de.westnordost.streetcomplete.data.download.tiles.TilePos +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.osm.mapdata.toPolygon +import de.westnordost.streetcomplete.resources.* +import de.westnordost.streetcomplete.screens.main.map.toPosition +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.maplibre.spatialk.geojson.Polygon +import org.jetbrains.compose.resources.painterResource +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.layers.FillLayer +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.util.MaplibreComposable + +/** Displays which areas have (not) been downloaded. Adds a hatching to the whole world except the + * downloaded areas. */ +@Composable @MaplibreComposable +fun DownloadedAreaLayer(tiles: Collection) { + val polygon by produceState(WORLD_POLYGON, tiles) { + value = withContext(Dispatchers.Default) { tiles.toHolesInWorldPolygon() } + } + val source = rememberGeoJsonSource(data = GeoJsonData.Features(polygon)) + + FillLayer( + id = "downloaded-area", + source = source, + opacity = const(0.6f), + pattern = image(painterResource(Res.drawable.downloaded_area_hatching)), + ) +} + +/** convert the given tile positions into a polygon that spans the whole world but has holes at + * where the tiles are at. */ +private fun Collection.toHolesInWorldPolygon(): Polygon { + val zoom = ApplicationConstants.DOWNLOAD_TILE_ZOOM + val world = listOf( + LatLon(+90.0, -180.0), + LatLon(-90.0, -180.0), + LatLon(-90.0, +180.0), + LatLon(+90.0, +180.0), + LatLon(+90.0, -180.0), + ) + val holes = this.map { it.asBoundingBox(zoom).toPolygon().asReversed() } + val polygons = listOf(world) + holes + return Polygon(polygons.map { polygon -> polygon.map { it.toPosition() } }) +} + +private val WORLD_POLYGON = emptyList().toHolesInWorldPolygon() diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/FocusedGeometryLayers.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/FocusedGeometryLayers.kt new file mode 100644 index 00000000000..be2c85ae280 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/FocusedGeometryLayers.kt @@ -0,0 +1,80 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.screens.main.map.isArea +import de.westnordost.streetcomplete.screens.main.map.isLines +import de.westnordost.streetcomplete.screens.main.map.isPoint +import de.westnordost.streetcomplete.screens.main.map.toGeometry +import org.maplibre.compose.expressions.dsl.any +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.feature +import org.maplibre.compose.expressions.dsl.not +import org.maplibre.compose.expressions.value.LineCap +import org.maplibre.compose.expressions.value.LineJoin +import org.maplibre.compose.layers.CircleLayer +import org.maplibre.compose.layers.FillLayer +import org.maplibre.compose.layers.LineLayer +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.util.MaplibreComposable +import kotlin.math.PI +import kotlin.math.cos + +/** Display focused element geometry. The focused element geometry is highlighted with a sort of + * breathing (sinusoid) animation */ +@MaplibreComposable +@Composable +fun FocusedGeometryLayers(geometry: ElementGeometry) { + + val highlightColor = MaterialTheme.colors.secondary + // breathing effect for highlight + val highlightTransition = rememberInfiniteTransition() + val highlight by highlightTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable(tween(1200, 0, LinearEasing)) + ) + val breathing = -cos(highlight * 2 * PI) / 2.0 + 0.5 // 0..1 + val opacity = ((1 - breathing) * 0.5 + 0.5).toFloat() // 1 .. 0.5 + val lineWidth = ((breathing + 1) * 8).dp // 8..16 + val circleRadius = ((breathing + 1) * 10).dp // 10..20 + + val source = rememberGeoJsonSource( + data = GeoJsonData.Features(geometry.toGeometry()) + ) + + FillLayer( + id = "focus-geo-fill", + source = source, + filter = feature.isArea(), + opacity = const(0.3f), + color = const(highlightColor), + ) + LineLayer( + id = "focus-geo-lines", + source = source, + filter = !feature.isPoint(), + opacity = const(opacity), + color = const(highlightColor), + width = const(lineWidth), + cap = const(LineCap.Round), + join = const(LineJoin.Round) + ) + CircleLayer( + id = "focus-geo-circle", + source = source, + filter = feature.isPoint(), + opacity = const(opacity), + color = const(highlightColor), + radius = const(circleRadius), + ) +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/GeometryMarkersLayers.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/GeometryMarkersLayers.kt new file mode 100644 index 00000000000..e0af985fc6a --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/GeometryMarkersLayers.kt @@ -0,0 +1,90 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.preset_maki_circle +import de.westnordost.streetcomplete.screens.main.map.byZoom +import de.westnordost.streetcomplete.screens.main.map.isArea +import de.westnordost.streetcomplete.screens.main.map.isPoint +import de.westnordost.streetcomplete.screens.main.map.mapIconImage +import de.westnordost.streetcomplete.ui.common.quest.Marker +import de.westnordost.streetcomplete.ui.ktx.id +import de.westnordost.streetcomplete.ui.theme.GeometryMarker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import org.maplibre.compose.expressions.dsl.case +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.convertToString +import org.maplibre.compose.expressions.dsl.feature +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.dsl.not +import org.maplibre.compose.expressions.dsl.switch +import org.maplibre.compose.expressions.dsl.textOffset +import org.maplibre.compose.expressions.value.LineCap +import org.maplibre.compose.expressions.value.LineJoin +import org.maplibre.compose.expressions.value.SymbolAnchor +import org.maplibre.compose.layers.FillLayer +import org.maplibre.compose.layers.LineLayer +import org.maplibre.compose.layers.SymbolLayer +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.util.MaplibreComposable +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.FeatureCollection +import org.maplibre.spatialk.geojson.Geometry + +/** Displays some generic geometry markers with an optional icon on the map. This is used to + * show the geometry of elements surrounding the selected quest */ +@MaplibreComposable +@Composable +fun GeometryMarkersLayers(markers: Collection, haloColor: Color) { + val features by produceState>>(emptyList(), markers) { + value = withContext(Dispatchers.Default) { markers.flatMap { it.toGeoJsonFeature() } } + } + val source = rememberGeoJsonSource( + data = GeoJsonData.Features(FeatureCollection(features)) + ) + + FillLayer( + id = "geo-fill", + source = source, + filter = feature.isArea(), + opacity = const(0.3f), + color = const(Color.GeometryMarker), + ) + LineLayer( + id = "geo-lines", + source = source, + filter = !feature.isPoint(), + opacity = const(0.5f), + color = const(Color.GeometryMarker), + width = const(10.dp), + cap = const(LineCap.Round), + join = const(LineJoin.Round) + ) + val markerImages = markers.map { it.icon ?: Res.drawable.preset_maki_circle }.distinct().map { icon -> + case("marker_" + icon.id, mapIconImage(icon, Color.GeometryMarker, haloColor)) + } + SymbolLayer( + id = "geo-symbols", + source = source, + filter = feature.isPoint(), + iconImage = switch(feature["icon"].convertToString(), markerImages, image("")), + iconSize = byZoom(17 to 0.5f, 19 to 1f), + iconAllowOverlap = const(true), + textField = feature["label"].convertToString(), + textColor = const(Color.GeometryMarker), + textSize = const(16.sp), + textFont = const(listOf("Roboto Bold")), + textAnchor = const(SymbolAnchor.Top), + textOffset = textOffset(0.em, 1.em), + textOptional = const(true), + ) +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/Marker.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/Marker.kt new file mode 100644 index 00000000000..c4feffb0ddf --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/Marker.kt @@ -0,0 +1,34 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry +import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry +import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry +import de.westnordost.streetcomplete.screens.main.map.toGeometry +import de.westnordost.streetcomplete.ui.common.quest.Marker +import de.westnordost.streetcomplete.ui.ktx.id +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.Geometry +import kotlin.collections.set + +fun Marker.toGeoJsonFeature(): List> { + val features = ArrayList>(3) + // point marker or any marker with title or icon + if (icon != null || title != null || geometry is ElementPointGeometry) { + val p = HashMap(2) + + p["icon"] = JsonPrimitive("marker_" + (icon?.id ?: "preset_maki_circle")) + if (title != null) { + p["label"] = JsonPrimitive(title) + } + features.add(Feature(geometry.center.toGeometry(), JsonObject(p))) + } + + // polygon / polylines marker(s) + if (geometry is ElementPolygonsGeometry || geometry is ElementPolylinesGeometry) { + features.add(Feature(geometry.toGeometry(), JsonObject(emptyMap()))) + } + return features +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/Pin.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/Pin.kt new file mode 100644 index 00000000000..2a82737aa95 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/Pin.kt @@ -0,0 +1,31 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.screens.main.map.toGeometry +import de.westnordost.streetcomplete.ui.ktx.id +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.jetbrains.compose.resources.DrawableResource +import org.maplibre.spatialk.geojson.Feature + +/** Intermediate data structure for display of pins on the map. The [icon] of a Pin shall be drawn + * with a [PinPainter][de.westnordost.streetcomplete.screens.main.map.PinPainter] */ +data class Pin( + val position: LatLon, + val icon: DrawableResource, + val properties: List> = emptyList(), + val order: Int = 0, +) + +fun Pin.toGeoJsonFeature() = + Feature( + geometry = position.toGeometry(), + properties = + JsonObject( + (listOf( + "icon-image" to JsonPrimitive("pin_" + icon.id), + "icon-order" to JsonPrimitive(order + 50), + ) + properties).toMap() + ) + ) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/PinsLayers.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/PinsLayers.kt new file mode 100644 index 00000000000..7adba763a12 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/PinsLayers.kt @@ -0,0 +1,191 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.pin_circle +import de.westnordost.streetcomplete.screens.main.map.pinPainter +import de.westnordost.streetcomplete.screens.main.map.toGeoJsonBoundingBox +import de.westnordost.streetcomplete.ui.ktx.id +import de.westnordost.streetcomplete.util.math.enclosingBoundingBox +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import org.jetbrains.compose.resources.painterResource +import org.maplibre.compose.expressions.dsl.all +import org.maplibre.compose.expressions.dsl.any +import org.maplibre.compose.expressions.dsl.case +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.convertToNumber +import org.maplibre.compose.expressions.dsl.convertToString +import org.maplibre.compose.expressions.dsl.div +import org.maplibre.compose.expressions.dsl.feature +import org.maplibre.compose.expressions.dsl.gt +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.dsl.log2 +import org.maplibre.compose.expressions.dsl.lte +import org.maplibre.compose.expressions.dsl.offset +import org.maplibre.compose.expressions.dsl.plus +import org.maplibre.compose.expressions.dsl.sp +import org.maplibre.compose.expressions.dsl.switch +import org.maplibre.compose.expressions.dsl.textOffset +import org.maplibre.compose.expressions.dsl.zoom +import org.maplibre.compose.expressions.value.TranslateAnchor +import org.maplibre.compose.interaction.ClickResult +import org.maplibre.compose.layers.CircleLayer +import org.maplibre.compose.layers.SymbolLayer +import org.maplibre.compose.map.LocalMapState +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.GeoJsonOptions +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.style.StyleHandleException +import org.maplibre.compose.util.DpPadding +import org.maplibre.compose.util.MaplibreComposable +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.FeatureCollection +import org.maplibre.spatialk.geojson.Geometry +import org.maplibre.spatialk.geojson.Point +import kotlin.time.Duration.Companion.milliseconds + +/** Display pins on the map, e.g. quest pins or pins for recent edits */ +@MaplibreComposable +@Composable +fun PinsLayers( + pins: Collection, + onClickPin: (properties: JsonObject) -> ClickResult, +) { + val mapState = checkNotNull(LocalMapState.current) + val coroutineScope = rememberCoroutineScope() + + val features by produceState>>(emptyList(), pins) { + value = withContext(Dispatchers.Default) { pins.map { it.toGeoJsonFeature() } } + } + val options = remember { + GeoJsonOptions( + cluster = true, + clusterMaxZoom = CLUSTER_MAX_ZOOM, + clusterRadius = 55, + ) + } + + val source = rememberGeoJsonSource( + data = GeoJsonData.Features(FeatureCollection(features)), + options = options + ) + + var clusterJob by remember { mutableStateOf(null) } + DisposableEffect(mapState.style.baseStyle, features) { + onDispose { clusterJob?.cancel() } + } + + fun onClickCluster(features: List>): ClickResult { + val feature = features.firstOrNull() ?: return ClickResult.Pass + val currentHandle = mapState.style.sources[source] ?: return ClickResult.Pass + clusterJob?.cancel() + clusterJob = coroutineScope.launch { + val leaves = try { + currentHandle.getClusterLeaves(feature, Long.MAX_VALUE, 0) + } catch (e: StyleHandleException) { + if (mapState.style.sources[source] !== currentHandle) return@launch + throw e + } + val positions = leaves.features.mapNotNull { (it.geometry as? Point)?.coordinates } + .map { LatLon(it.latitude, it.longitude) } + if (positions.isEmpty()) return@launch + val bounds = positions.enclosingBoundingBox() + val camera = mapState.cameraPosition + // TODO maplibre-compose: Query the fitted camera before animating to restore + // the 0.25 zoom margin, maximum zoom 19, and zoom-dependent duration. + // Requires a release containing https://github.com/maplibre/maplibre-compose/pull/1400. + mapState.animateCameraToBounds( + bounds.toGeoJsonBoundingBox(), + bearing = camera.bearing, + tilt = camera.tilt, + duration = 450.milliseconds, + ) + } + return ClickResult.Consume + } + + fun onClick(features: List>): ClickResult { + val properties = features.firstOrNull()?.properties ?: return ClickResult.Pass + return onClickPin(properties) + } + + SymbolLayer( + id = "pin-cluster-layer", + source = source, + minZoom = CLUSTER_MIN_ZOOM.toFloat(), + filter = all( + zoom().lte(const(CLUSTER_MAX_ZOOM)), + feature["point_count"].convertToNumber().gt(const(1)), + ), + iconImage = image(painterResource(Res.drawable.pin_circle)), + iconSize = const(0.5f) + (log2(feature["point_count"].convertToNumber()) / const(10f)), + iconAllowOverlap = const(true), + iconIgnorePlacement = const(true), + textField = feature["point_count"].convertToString(), + textSize = (const(15f) + log2(feature["point_count"].convertToNumber()) / const(1.5f)).sp, + textFont = const(listOf("Roboto Regular")), + textOffset = textOffset(0.em, 0.1.em), + textAllowOverlap = const(true), + textIgnorePlacement = const(true), + onClick = ::onClickCluster, + ) + CircleLayer( + id = "pin-dot-layer", + source = source, + minZoom = CLUSTER_MAX_ZOOM.toFloat(), + filter = any( + zoom().gt(const(CLUSTER_MAX_ZOOM)), + feature["point_count"].convertToNumber(const(1)).lte(const(1)), + ), + color = const(Color.White), + radius = const(5.dp), + strokeColor = const(Color(0xffaaaaaa)), + strokeWidth = const(1.dp), + translate = offset(0.dp, -8.dp), // so that it hides behind the pin + translateAnchor = const(TranslateAnchor.Viewport), + ) + val pinImages = pins.map { it.icon }.distinct().map { icon -> + case("pin_" + icon.id, image(pinPainter(painterResource(icon)), size = DpSize(71.dp, 71.dp))) + } + SymbolLayer( + id = "pins-layer", + source = source, + filter = zoom().gt(const(CLUSTER_MAX_ZOOM)), + sortKey = feature["icon-order"].convertToNumber(), + iconImage = switch(feature["icon-image"].convertToString(), pinImages, image("")), + // constant icon size because click area would become a bit too small and more + // importantly, dynamic size per zoom + collision doesn't work together well, it + // results in a lot of flickering. + iconSize = const(1f), + iconPadding = const(DpPadding( + left = 2.5.dp, + top = -2.5.dp, + right = 0.dp, + bottom = -7.dp, + )), + iconOffset = const(DpOffset((-4.5).dp, (-34.5).dp)), + iconAllowOverlap = const(false), + iconIgnorePlacement = const(false), + onClick = ::onClick, + ) +} + +private const val CLUSTER_MIN_ZOOM = 13 +private const val CLUSTER_MAX_ZOOM = 14 diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/SelectedPinsLayer.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/SelectedPinsLayer.kt new file mode 100644 index 00000000000..07236bf943b --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/SelectedPinsLayer.kt @@ -0,0 +1,62 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.screens.main.map.pinPainter +import de.westnordost.streetcomplete.screens.main.map.toPosition +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.layers.SymbolLayer +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.util.DpPadding +import org.maplibre.compose.util.MaplibreComposable +import org.maplibre.spatialk.geojson.MultiPoint + +/** Displays "selected" pins. Those pins should always be shown on top of pins displayed by + * [PinsLayers]. + * + * When they are shown, a short springy animation animate them to a larger size. + * */ +@MaplibreComposable +@Composable +fun SelectedPinsLayer(icon: DrawableResource, pinPositions: Collection) { + val pinsSize = remember { Animatable(0.5f) } + LaunchedEffect(pinPositions, icon) { + pinsSize.snapTo(0.5f) + pinsSize.animateTo( + targetValue = 1.5f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ), + ) + } + + val geometry = remember(pinPositions) { MultiPoint(pinPositions.map { it.toPosition() }) } + val source = rememberGeoJsonSource(data = GeoJsonData.Features(geometry)) + + SymbolLayer( + id = "selected-pins-layer", + source = source, + iconImage = image(pinPainter(painterResource(icon)), size = DpSize(71.dp, 71.dp)), + iconSize = const(pinsSize.value), + iconPadding = const(DpPadding( + left = 2.5.dp, + top = -2.5.dp, + right = 0.dp, + bottom = -7.dp, + )), + iconOffset = const(DpOffset((-4.5).dp, (-34.5).dp)), + ) +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/StyleableOverlayLayers.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/StyleableOverlayLayers.kt new file mode 100644 index 00000000000..513e4ff4f62 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/StyleableOverlayLayers.kt @@ -0,0 +1,221 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import de.westnordost.streetcomplete.screens.main.map.byZoom +import de.westnordost.streetcomplete.screens.main.map.inMeters +import de.westnordost.streetcomplete.screens.main.map.isArea +import de.westnordost.streetcomplete.screens.main.map.isLines +import de.westnordost.streetcomplete.screens.main.map.isPoint +import de.westnordost.streetcomplete.screens.main.map.mapIconImage +import de.westnordost.streetcomplete.ui.ktx.id +import kotlinx.serialization.json.JsonObject +import org.jetbrains.compose.resources.DrawableResource +import org.maplibre.compose.expressions.dsl.all +import org.maplibre.compose.expressions.dsl.asNumber +import org.maplibre.compose.expressions.dsl.case +import org.maplibre.compose.expressions.dsl.condition +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.convertToBoolean +import org.maplibre.compose.expressions.dsl.convertToColor +import org.maplibre.compose.expressions.dsl.convertToNumber +import org.maplibre.compose.expressions.dsl.convertToString +import org.maplibre.compose.expressions.dsl.feature +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.dsl.not +import org.maplibre.compose.expressions.dsl.offset +import org.maplibre.compose.expressions.dsl.step +import org.maplibre.compose.expressions.dsl.switch +import org.maplibre.compose.expressions.dsl.textOffset +import org.maplibre.compose.expressions.dsl.zoom +import org.maplibre.compose.expressions.value.LineCap +import org.maplibre.compose.expressions.value.LineJoin +import org.maplibre.compose.expressions.value.SymbolAnchor +import org.maplibre.compose.expressions.value.SymbolZOrder +import org.maplibre.compose.interaction.ClickResult +import org.maplibre.compose.layers.FillExtrusionLayer +import org.maplibre.compose.layers.FillLayer +import org.maplibre.compose.layers.LineLayer +import org.maplibre.compose.layers.SymbolLayer +import org.maplibre.compose.sources.VectorSource +import org.maplibre.compose.util.MaplibreComposable +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.Geometry + +/** Display styled map data labels */ +@MaplibreComposable +@Composable +fun StyleableOverlayLabelLayer( + source: VectorSource, + icons: Collection, + color: Color, + haloColor: Color, + onClickElement: (properties: JsonObject) -> ClickResult, +) { + val images = icons.distinct().map { icon -> + case(icon.id.orEmpty(), mapIconImage(icon, color, haloColor)) + } + SymbolLayer( + id = "overlay-symbols", + source = source, + minZoom = 17f, + filter = feature.isPoint(), + zOrder = const(SymbolZOrder.Source), + iconImage = switch(feature["icon"].convertToString(), images, image("")), + iconSize = byZoom(17 to 0.5f, 19 to 1f), + iconAllowOverlap = const(true), + textField = feature["label"].convertToString(), + textColor = const(color), + textHaloColor = const(haloColor), + textHaloWidth = const(2.5.dp), + textFont = const(listOf("Roboto Regular")), + textAnchor = const(SymbolAnchor.Top), + textOffset = switch( + condition(feature.has("icon"), textOffset(0.em, 1.em)), + fallback = textOffset(0.em, 0.em) + ), + textSize = const(16.sp), + textOptional = const(true), + textAllowOverlap = step( + input = zoom(), + fallback = const(false), + 21 to const(true) + ), + onClick = { onClick(it, onClickElement) } + ) +} + +/** Display styled map data */ +@MaplibreComposable @Composable +fun StyleableOverlayLayers( + source: VectorSource, + onClickElement: (properties: JsonObject) -> ClickResult, +) { + val dashed = feature["dashed"].convertToBoolean() + val opacity = feature["opacity"].convertToNumber() + val color = feature["color"].convertToColor() + val outlineColor = feature["outline-color"].convertToColor() + val width = inMeters(feature["width"].asNumber()) + val casingWidth = inMeters(0.5f) + + LineLayer( + id = "overlay-lines-casing", + source = source, + minZoom = MIN_ZOOM, + filter = all(feature.isLines(), !feature.has("offset"), !dashed), + opacity = opacity, + color = outlineColor, + width = casingWidth, + gapWidth = width, + cap = const(LineCap.Round), + join = const(LineJoin.Round), + ) + FillLayer( + id = "overlay-fills", + source = source, + minZoom = MIN_ZOOM, + filter = feature.isArea(), + opacity = opacity, + color = color, + onClick = { onClick(it, onClickElement) } + ) + LineLayer( + id = "overlay-lines", + source = source, + minZoom = MIN_ZOOM, + filter = all(feature.isLines(), !feature.has("offset"), !dashed), + opacity = opacity, + color = color, + width = width, + cap = const(LineCap.Round), + join = const(LineJoin.Round), + onClick = { onClick(it, onClickElement) } + ) + LineLayer( + id = "overlay-lines-dashed", + source = source, + minZoom = MIN_ZOOM, + filter = all(feature.isLines(), !feature.has("offset"), dashed), + opacity = opacity, + color = color, + width = width, + dasharray = const(listOf(1.5f, 1f)), + cap = const(LineCap.Butt), // because of dashed + join = const(LineJoin.Round), + onClick = { onClick(it, onClickElement) } + ) + LineLayer( + id = "overlay-fills-outline", + source = source, + minZoom = MIN_ZOOM, + filter = feature.isArea(), + opacity = opacity, + color = outlineColor, + width = casingWidth, + cap = const(LineCap.Round), + join = const(LineJoin.Round), + ) + FillExtrusionLayer( + id = "overlay-heights", + source = source, + minZoom = MIN_ZOOM, + filter = all(feature.isArea(), feature.has("height")), + // data-driven-styling not supported (see https://maplibre.org/maplibre-style-spec/layers/#fill-extrusion-opacity) + opacity = const(1f), // cannot use `opacity = opacity` + color = color, + height = feature["height"].convertToNumber(), + base = feature["min-height"].convertToNumber() + ) +} + +/** Display styled left-right-of-line map data */ +@MaplibreComposable @Composable +fun StyleableOverlaySideLayer(source: VectorSource, isBridge: Boolean) { + val bridge = feature["bridge"].convertToBoolean() + val dashed = feature["dashed"].convertToBoolean() + val opacity = feature["opacity"].convertToNumber() + val color = feature["color"].convertToColor() + val width = inMeters(feature["width"].asNumber()) + val offset = inMeters(feature["offset"].asNumber()) + val bridgeString = if (isBridge) "-bridge" else "" + val bridgeMatch = if (isBridge) bridge else !bridge + + LineLayer( + id = "overlay-lines-side" + bridgeString, + source = source, + minZoom = MIN_ZOOM, + filter = all(feature.isLines(), feature.has("offset"), bridgeMatch, !dashed), + color = color, + width = width, + opacity = opacity, + offset = offset, + cap = const(LineCap.Round), + join = const(LineJoin.Round), + ) + LineLayer( + id = "overlay-lines-side-dashed" + bridgeString, + source = source, + minZoom = MIN_ZOOM, + filter = all(feature.isLines(), feature.has("offset"), bridgeMatch, dashed), + color = color, + width = width, + opacity = opacity, + offset = offset, + dasharray = const(listOf(1.5f, 1f)), + cap = const(LineCap.Butt), // because of dashed + join = const(LineJoin.Round), + ) +} + +private inline fun onClick( + features: List>, + onClickElement: (properties: JsonObject) -> ClickResult +): ClickResult { + val properties = features.firstOrNull()?.properties ?: return ClickResult.Pass + return onClickElement(properties) +} + +private val MIN_ZOOM = 14f diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/StyledElement.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/StyledElement.kt new file mode 100644 index 00000000000..f6f7843b8f9 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/StyledElement.kt @@ -0,0 +1,194 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.Element +import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey +import de.westnordost.streetcomplete.data.osm.mapdata.ElementType +import de.westnordost.streetcomplete.data.osm.mapdata.key +import de.westnordost.streetcomplete.data.overlays.OverlayStyle +import de.westnordost.streetcomplete.screens.main.map.toGeometry +import de.westnordost.streetcomplete.ui.ktx.id +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull +import org.jetbrains.compose.resources.DrawableResource +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.Geometry + +/** Intermediate data structure to be fed into the [StyleableOverlayLayers] */ +data class StyledElement( + val element: Element, + val geometry: ElementGeometry, + val style: OverlayStyle +) + +fun StyledElement.toGeoJsonFeatures(): List> { + val p = createProperties(element.key, style.disabled) + + return when (style) { + is OverlayStyle.Point -> { + if (style.icon != null) { + p["icon"] = JsonPrimitive(style.icon.id) + } + if (style.label != null) { + p["label"] = JsonPrimitive(style.label) + } + + listOf(Feature(geometry.center.toGeometry(), JsonObject(p))) + } + is OverlayStyle.Polygon -> { + if (style.color.alpha != 0f) { + p["color"] = JsonPrimitive(style.color.toRgbaString()) + p["outline-color"] = JsonPrimitive(style.color.darkened().toRgbaString()) + p["opacity"] = JsonPrimitive(0.8f) + } else { + p["opacity"] = JsonPrimitive(0f) + } + + if (style.height != null && style.color.alpha != 0f) { + p["height"] = JsonPrimitive(style.height) + if (style.minHeight != null) { + p["min-height"] = JsonPrimitive(style.minHeight.coerceAtMost(style.height)) + } + } + + val f = Feature(geometry.toGeometry(), JsonObject(p)) + val point = if (style.label != null || style.icon != null) { + val pp = createProperties(element.key, style.disabled) + if (style.icon != null) { + pp["icon"] = JsonPrimitive(style.icon.id) + } + if (style.label != null) { + pp["label"] = JsonPrimitive(style.label) + } + Feature(geometry.center.toGeometry(), JsonObject(pp)) + } else { + null + } + + listOfNotNull(f, point) + } + is OverlayStyle.Polyline -> { + val line = geometry.toGeometry() + val width = getLineWidth(element.tags) + if (isBridge(element.tags)) { + p["bridge"] = JsonPrimitive(true) + } + + val left = style.strokeLeft?.let { + val p2 = HashMap(p) + p2["width"] = JsonPrimitive(3f) + p2["offset"] = JsonPrimitive(-(width / 2f + 1.5f)) + if (it.color.alpha != 0f) { + p2["color"] = JsonPrimitive(it.color.toRgbaString()) + p2["opacity"] = JsonPrimitive(1f) + } else { + p2["opacity"] = JsonPrimitive(0f) + } + if (it.dashed) { + p2["dashed"] = JsonPrimitive(true) + } + Feature(line, JsonObject(p2)) + } + + val right = style.strokeRight?.let { + val p2 = HashMap(p) + p2["width"] = JsonPrimitive(3f) + p2["offset"] = JsonPrimitive(+(width / 2f + 1.5f)) + if (it.color.alpha != 0f) { + p2["color"] = JsonPrimitive(it.color.toRgbaString()) + p2["opacity"] = JsonPrimitive(1f) + } else { + p2["opacity"] = JsonPrimitive(0f) + } + if (it.dashed) { + p2["dashed"] = JsonPrimitive(true) + } + Feature(line, JsonObject(p2)) + } + + val center = style.stroke.let { + val p2 = HashMap(p) + p2["width"] = JsonPrimitive(width) + if (it != null && it.color.alpha != 0f) { + p2["color"] = JsonPrimitive(it.color.toRgbaString()) + p2["outline-color"] = JsonPrimitive(it.color.darkened().toRgbaString()) + p2["opacity"] = JsonPrimitive(1f) + } else { + p2["opacity"] = JsonPrimitive(0f) + } + if (it?.dashed == true) { + p2["dashed"] = JsonPrimitive(true) + } + Feature(line, JsonObject(p2)) + } + + val label = if (style.label != null) { + Feature( + geometry.center.toGeometry(), + JsonObject(createProperties(element.key, style.disabled) + ("label" to JsonPrimitive(style.label))) + ) + } else { + null + } + + listOfNotNull(left, right, center, label) + } + } +} + +fun JsonObject.toElementKey(): ElementKey? { + val id = get(ELEMENT_ID)?.jsonPrimitive?.longOrNull ?: return null + val type = get(ELEMENT_TYPE)?.jsonPrimitive?.content ?: return null + return ElementKey(ElementType.entries.find { it.name == type } ?: return null, id) +} +fun JsonObject.isDisabled(): Boolean = + get(DISABLED)?.jsonPrimitive?.booleanOrNull ?: false + +private fun createProperties(key: ElementKey, disabled: Boolean): MutableMap { + val p = HashMap() + p[ELEMENT_ID] = JsonPrimitive(key.id) + p[ELEMENT_TYPE] = JsonPrimitive(key.type.name) + if (disabled) p[DISABLED] = JsonPrimitive(disabled) + return p +} + +/** mimics width of line as seen in StreetComplete map style */ +private fun getLineWidth(tags: Map): Float = when (tags["highway"]) { + "motorway" -> 8f + "motorway_link" -> 4f + "trunk", "primary", "secondary", "tertiary" -> 6f + "service", "track", "busway" -> 3f + "path", "cycleway", "footway", "bridleway", "steps" -> 1.0f + null -> 2f + else -> 4f +} + +private fun isBridge(tags: Map): Boolean = + tags["bridge"] != null && tags["bridge"] != "no" + +fun OverlayStyle.getIcon(): DrawableResource? = when (this) { + is OverlayStyle.Point -> icon + is OverlayStyle.Polygon -> icon + is OverlayStyle.Polyline -> null +} + +private fun Color.darkened(): Color = Color( + red = red * 0.67f, + green = green * 0.67f, + blue = blue * 0.67f, + alpha = alpha +) + +private fun Color.toRgbaString(): String { + val c = toArgb() + return "rgba(${(c shr 16) and 0xFF}, ${(c shr 8) and 0xFF}, ${c and 0xFF}, ${alpha})" +} + +private const val ELEMENT_TYPE = "element_type" +private const val ELEMENT_ID = "element_id" +private const val DISABLED = "disabled" diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/TracksLayer.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/TracksLayer.kt new file mode 100644 index 00000000000..aa12d9d3cbe --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/layers/TracksLayer.kt @@ -0,0 +1,160 @@ +package de.westnordost.streetcomplete.screens.main.map.layers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.resources.* +import de.westnordost.streetcomplete.screens.main.map.animateLatLonAsState +import de.westnordost.streetcomplete.screens.main.map.toLineGeometry +import de.westnordost.streetcomplete.screens.main.map.toMultiLineGeometry +import de.westnordost.streetcomplete.screens.main.map.toPosition +import de.westnordost.streetcomplete.ui.theme.Location +import de.westnordost.streetcomplete.ui.theme.Recording +import de.westnordost.streetcomplete.util.ktx.isApril1st +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.compose.resources.painterResource +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.value.LineCap +import org.maplibre.compose.layers.LineLayer +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.VectorSource +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.util.MaplibreComposable +import org.maplibre.spatialk.geojson.Geometry +import org.maplibre.spatialk.geojson.GeometryCollection +import org.maplibre.spatialk.geojson.LineString + +/** Display the path(s) walked on the map. + * + * The current [trackpoints] will be shown in red when the user [isRecording], otherwise blue. + * The last segment of [trackpoints] is animated from the second last to the last position, which + * is done in-sync with the moving of the location puck. + * + * Since the list of trackpoints is changed every time the location puck moves to a new location, + * for performance reasons, when the list of trackpoints gets too long, the last X trackpoints + * can be cut off (except if the user [isRecording]) and added to [oldTrackpointsLists]. These are + * displayed with less opacity and need to be updated less often. */ +@MaplibreComposable @Composable +fun TracksLayers( + trackpoints: List, + isRecording: Boolean, + oldTrackpointsLists: List>, +) { + // let's not check for the date on every recomposition :-) + val isApril1st = remember { isApril1st() } + + val trackLastSegment = remember(trackpoints) { + if (trackpoints.size >= 2) trackpoints.takeLast(2) else null + } + val trackWithoutLast = remember(trackpoints) { + if (trackpoints.size > 1) trackpoints.take(trackpoints.size - 1) else emptyList() + } + + val trackData by produceState(EMPTY_GEOMETRY, trackWithoutLast) { + value = withContext(Dispatchers.Default) { + trackWithoutLast.toLineGeometry() ?: EMPTY_GEOMETRY + } + } + val tracksSource = rememberGeoJsonSource(data = GeoJsonData.Features(trackData)) + + // we want to animate the drawing of the track from the last position to the current position + // while the position marker animates at the same time from the last position to the current + // position (see CurrentLocationLayers) + val animatedData = + if (trackLastSegment != null) { + val animatedLastPosition by animateLatLonAsState(targetValue = trackLastSegment.last()) + LineString(trackLastSegment.first().toPosition(), animatedLastPosition.toPosition()) + } else { + EMPTY_GEOMETRY + } + val animatedTracksSource = rememberGeoJsonSource(data = GeoJsonData.Features(animatedData)) + + // old tracks are expected to not update so often + val oldTrackData by produceState(EMPTY_GEOMETRY, oldTrackpointsLists) { + value = withContext(Dispatchers.Default) { oldTrackpointsLists.toMultiLineGeometry() } + } + val oldTracksSource = rememberGeoJsonSource(data = GeoJsonData.Features(oldTrackData)) + + // old tracks are drawn with less alpha so the map stays well visible + TracksLayer( + id = "old-track", + source = oldTracksSource, + opacity = 0.2f, + isApril1st = isApril1st, + ) + + TracksLayer( + id = "track", + source = tracksSource, + isRecording = isRecording, + isApril1st = isApril1st, + ) + + TracksLayer( + id = "animate-track", + source = animatedTracksSource, + isRecording = isRecording, + isApril1st = isApril1st, + ) +} + +/** Displays a path(s) walked on the map */ +@MaplibreComposable @Composable +private fun TracksLayer( + id: String, + source: VectorSource, + isRecording: Boolean = false, + opacity: Float = 0.6f, + isApril1st: Boolean = false, +) { + if (isApril1st) { + TracksLayerApril1st(id, source, isRecording, opacity) + } else { + TracksLayerDefault(id, source, isRecording, opacity) + } +} + +@MaplibreComposable @Composable +private fun TracksLayerApril1st( + id: String, + source: VectorSource, + isRecording: Boolean, + opacity: Float, +) { + LineLayer( + id = id, + source = source, + opacity = const(opacity), + width = const(26.dp), + pattern = image(painterResource( + if (isRecording) Res.drawable.track_nyan_record + else Res.drawable.track_nyan + )), + ) +} + +@MaplibreComposable @Composable +private fun TracksLayerDefault( + id: String, + source: VectorSource, + isRecording: Boolean, + opacity: Float, +) { + LineLayer( + id = id, + source = source, + opacity = const(opacity), + cap = const(LineCap.Round), + dasharray = const(listOf(0, 2)), + width = const(6.dp), + color = const(if (isRecording) Color.Recording else Color.Location), + ) +} + +private val EMPTY_GEOMETRY: Geometry = GeometryCollection(emptyList()) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Camera.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Camera.kt deleted file mode 100644 index 82802312cdb..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/maplibre/Camera.kt +++ /dev/null @@ -1,38 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.map.maplibre - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.LayoutDirection -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon - -/** State of the camera */ -data class CameraPosition( - val position: LatLon, - val rotation: Double, - val tilt: Double, - val zoom: Double, - val padding: Padding? = null -) -data class Padding(val left: Double, val top: Double, val right: Double, val bottom: Double) - -/** Builder data class for camera updates */ -class CameraUpdate { - var position: LatLon? = null - var rotation: Double? = null // degrees - var tilt: Double? = null // degrees - var zoom: Double? = null - var padding: Padding? = null - - var zoomBy: Double? = null - var tiltBy: Double? = null - var rotationBy: Double? = null -} - -fun PaddingValues.toPadding(layoutDirection: LayoutDirection, density: Density) = with(density) { - Padding( - left = calculateLeftPadding(layoutDirection).toPx().toDouble(), - top = calculateTopPadding().toPx().toDouble(), - right = calculateRightPadding(layoutDirection).toPx().toDouble(), - bottom = calculateBottomPadding().toPx().toDouble() - ) -} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/EditHistoryPinsSource.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/EditHistoryPinsSource.kt new file mode 100644 index 00000000000..f6f4457afb6 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/EditHistoryPinsSource.kt @@ -0,0 +1,113 @@ +package de.westnordost.streetcomplete.screens.main.map.sources + +import de.westnordost.streetcomplete.data.edithistory.Edit +import de.westnordost.streetcomplete.data.edithistory.EditHistorySource +import de.westnordost.streetcomplete.data.edithistory.EditKey +import de.westnordost.streetcomplete.data.edithistory.ElementEditKey +import de.westnordost.streetcomplete.data.edithistory.NoteEditKey +import de.westnordost.streetcomplete.data.edithistory.QuestHiddenKey +import de.westnordost.streetcomplete.data.osm.edits.ElementEdit +import de.westnordost.streetcomplete.data.osm.mapdata.ElementType +import de.westnordost.streetcomplete.data.osm.osmquests.OsmQuestHidden +import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEdit +import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestHidden +import de.westnordost.streetcomplete.data.quest.OsmNoteQuestKey +import de.westnordost.streetcomplete.data.quest.OsmQuestKey +import de.westnordost.streetcomplete.screens.main.edithistory.icon +import de.westnordost.streetcomplete.screens.main.map.layers.Pin +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long + +class EditHistoryPinsSource( + private val editHistorySource: EditHistorySource +) { + @OptIn(ExperimentalCoroutinesApi::class) + val pins: Flow> = callbackFlow { + val listener = object : EditHistorySource.Listener { + override fun onAdded(added: Edit) { trySend(Unit) } + override fun onSynced(synced: Edit) { } + override fun onDeleted(deleted: List) { trySend(Unit) } + override fun onInvalidated() { trySend(Unit) } + } + // Subscribe before reading so edits made during the initial load are not lost. + editHistorySource.addListener(listener) + trySend(Unit) + awaitClose { editHistorySource.removeListener(listener) } + }.buffer(Channel.CONFLATED).mapLatest { + withContext(Dispatchers.IO) { + editHistorySource.getAll().mapIndexed { index, edit -> edit.toEditPin(index) } + } + } + + fun getEditKey(properties: JsonObject): EditKey? = + properties.toEditKey() +} + +private const val MARKER_EDIT_TYPE = "edit_type" + +private const val MARKER_ELEMENT_TYPE = "element_type" +private const val MARKER_ELEMENT_ID = "element_id" +private const val MARKER_QUEST_TYPE = "quest_type" +private const val MARKER_NOTE_ID = "note_id" +private const val MARKER_ID = "id" + +private const val EDIT_TYPE_ELEMENT = "element" +private const val EDIT_TYPE_NOTE = "note" +private const val EDIT_TYPE_HIDE_OSM_NOTE_QUEST = "hide_osm_note_quest" +private const val EDIT_TYPE_HIDE_OSM_QUEST = "hide_osm_quest" + +private fun Edit.toEditPin(order: Int) = Pin(position, icon!!, toProperties(), order) + +private fun Edit.toProperties(): List> = when (this) { + is ElementEdit -> listOf( + MARKER_EDIT_TYPE to JsonPrimitive(EDIT_TYPE_ELEMENT), + MARKER_ID to JsonPrimitive(id) + ) + is NoteEdit -> listOf( + MARKER_EDIT_TYPE to JsonPrimitive(EDIT_TYPE_NOTE), + MARKER_ID to JsonPrimitive(id) + ) + is OsmNoteQuestHidden -> listOf( + MARKER_EDIT_TYPE to JsonPrimitive(EDIT_TYPE_HIDE_OSM_NOTE_QUEST), + MARKER_NOTE_ID to JsonPrimitive(note.id) + ) + is OsmQuestHidden -> listOf( + MARKER_EDIT_TYPE to JsonPrimitive(EDIT_TYPE_HIDE_OSM_QUEST), + MARKER_ELEMENT_TYPE to JsonPrimitive(elementType.name), + MARKER_ELEMENT_ID to JsonPrimitive(elementId), + MARKER_QUEST_TYPE to JsonPrimitive(questType.name) + ) + else -> throw IllegalArgumentException() +} + +private fun JsonObject.toEditKey(): EditKey? { + val editType = get(MARKER_EDIT_TYPE)?.jsonPrimitive?.contentOrNull + return when (editType) { + EDIT_TYPE_ELEMENT -> + ElementEditKey(getValue(MARKER_ID).jsonPrimitive.long) + EDIT_TYPE_NOTE -> + NoteEditKey(getValue(MARKER_ID).jsonPrimitive.long) + EDIT_TYPE_HIDE_OSM_QUEST -> + QuestHiddenKey(OsmQuestKey( + ElementType.valueOf(getValue(MARKER_ELEMENT_TYPE).jsonPrimitive.content), + getValue(MARKER_ELEMENT_ID).jsonPrimitive.long, + getValue(MARKER_QUEST_TYPE).jsonPrimitive.content + )) + EDIT_TYPE_HIDE_OSM_NOTE_QUEST -> + QuestHiddenKey(OsmNoteQuestKey(getValue(MARKER_NOTE_ID).jsonPrimitive.long)) + else -> null + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/MapQuestPinsSource.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/MapQuestPinsSource.kt new file mode 100644 index 00000000000..fda4862ea9d --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/MapQuestPinsSource.kt @@ -0,0 +1,175 @@ +package de.westnordost.streetcomplete.screens.main.map.sources + +import de.westnordost.streetcomplete.data.download.tiles.TilesRect +import de.westnordost.streetcomplete.data.download.tiles.enclosingTilesRect +import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox +import de.westnordost.streetcomplete.data.osm.mapdata.ElementType +import de.westnordost.streetcomplete.data.quest.OsmNoteQuestKey +import de.westnordost.streetcomplete.data.quest.OsmQuestKey +import de.westnordost.streetcomplete.data.quest.Quest +import de.westnordost.streetcomplete.data.quest.QuestKey +import de.westnordost.streetcomplete.data.quest.QuestType +import de.westnordost.streetcomplete.data.quest.QuestTypeRegistry +import de.westnordost.streetcomplete.data.quest.VisibleQuestsSource +import de.westnordost.streetcomplete.data.visiblequests.QuestTypeOrderSource +import de.westnordost.streetcomplete.screens.main.map.layers.Pin +import de.westnordost.streetcomplete.util.math.contains +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long + +class MapQuestPinsSource( + private val questTypeOrderSource: QuestTypeOrderSource, + private val questTypeRegistry: QuestTypeRegistry, + private val visibleQuestsSource: VisibleQuestsSource +) { + private val displayedRect = MutableStateFlow(null) + + // Only candidate keys survive inactivity; their current visibility and data are re-read. + private var multiMarkerQuestKeys = emptySet() + + val pins: Flow> = channelFlow { + val questsInView = mutableMapOf>() + displayedRect.collectLatest { rect -> + if (rect == null) { + questsInView.clear() + send(emptyList()) + return@collectLatest + } + val bbox = rect.asBoundingBox(TILES_ZOOM) + var orders = emptyMap() + events().collect { event -> + when (event) { + Event.Reload -> { + val (quests, questOrders) = withContext(Dispatchers.IO) { + val types = questTypeRegistry.toMutableList() + questTypeOrderSource.sort(types) + // A long quest can have a visible marker but its center outside this view. + val retained = multiMarkerQuestKeys.mapNotNull { visibleQuestsSource.get(it) } + .filter { quest -> quest.markerLocations.any { it in bbox } } + (retained + visibleQuestsSource.getAll(bbox)) to + types.withIndex().associate { it.value to it.index } + } + orders = questOrders + questsInView.clear() + quests.forEach { questsInView[it.key] = it.toPins(orders) } + } + is Event.Updated -> { + event.removed.forEach { questsInView.remove(it) } + event.added.forEach { quest -> + if (quest.markerLocations.any { it in bbox }) { + questsInView[quest.key] = quest.toPins(orders) + } else { + questsInView.remove(quest.key) + } + } + } + } + multiMarkerQuestKeys = questsInView.filterValues { it.size > 1 }.keys.toSet() + send(questsInView.values.flatten()) + } + } + }.flowOn(Dispatchers.Default) + + // Callbacks may arrive on different threads; only the collector mutates the displayed data. + private fun events(): Flow = callbackFlow { + val questsListener = object : VisibleQuestsSource.Listener { + override fun onUpdated(added: Collection, removed: Collection) { + trySend(Event.Updated(added.toList(), removed.toList())) + } + override fun onInvalidated() { trySend(Event.Reload) } + } + val orderListener = object : QuestTypeOrderSource.Listener { + override fun onQuestTypeOrderAdded(item: QuestType, toAfter: QuestType) { + trySend(Event.Reload) + } + override fun onQuestTypeOrdersChanged() { trySend(Event.Reload) } + } + visibleQuestsSource.addListener(questsListener) + questTypeOrderSource.addListener(orderListener) + trySend(Event.Reload) + awaitClose { + visibleQuestsSource.removeListener(questsListener) + questTypeOrderSource.removeListener(orderListener) + } + }.buffer(Channel.UNLIMITED) + + fun onMapMoved(zoom: Double, displayedArea: BoundingBox?) { + if (displayedArea == null) { + displayedRect.value = null + return + } + // Keep the loaded data when zooming out, including clusters at zoom 13–14. + if (zoom < 14) return + val rect = displayedArea.enclosingTilesRect(TILES_ZOOM) + if (rect.size > 32) return + if (displayedRect.value?.contains(rect) != true) displayedRect.value = rect + } + + fun getQuestKey(properties: JsonObject): QuestKey? = properties.toQuestKey() + + private fun Quest.toPins(orders: Map): List = + markerLocations.map { Pin(it, type.icon, key.toProperties(), orders[type] ?: 0) } + + private sealed interface Event { + data object Reload : Event + data class Updated(val added: List, val removed: List) : Event + } + + companion object { + private const val TILES_ZOOM = 16 + } +} + + +private const val MARKER_QUEST_GROUP = "quest_group" + +private const val MARKER_ELEMENT_TYPE = "element_type" +private const val MARKER_ELEMENT_ID = "element_id" +private const val MARKER_QUEST_TYPE = "quest_type" +private const val MARKER_NOTE_ID = "note_id" + +private const val QUEST_GROUP_OSM = "osm" +private const val QUEST_GROUP_OSM_NOTE = "osm_note" + +private fun QuestKey.toProperties(): List> = when (this) { + is OsmNoteQuestKey -> listOf( + MARKER_QUEST_GROUP to JsonPrimitive(QUEST_GROUP_OSM_NOTE), + MARKER_NOTE_ID to JsonPrimitive(noteId) + ) + is OsmQuestKey -> listOf( + MARKER_QUEST_GROUP to JsonPrimitive(QUEST_GROUP_OSM), + MARKER_ELEMENT_TYPE to JsonPrimitive(elementType.name), + MARKER_ELEMENT_ID to JsonPrimitive(elementId), + MARKER_QUEST_TYPE to JsonPrimitive(questTypeName) + ) +} + +private fun JsonObject.toQuestKey(): QuestKey? { + val questGroup = get(MARKER_QUEST_GROUP)?.jsonPrimitive?.contentOrNull + return when (questGroup) { + QUEST_GROUP_OSM_NOTE -> + OsmNoteQuestKey(getValue(MARKER_NOTE_ID).jsonPrimitive.long) + QUEST_GROUP_OSM -> + OsmQuestKey( + ElementType.valueOf(getValue(MARKER_ELEMENT_TYPE).jsonPrimitive.content), + getValue(MARKER_ELEMENT_ID).jsonPrimitive.long, + getValue(MARKER_QUEST_TYPE).jsonPrimitive.content + ) + else -> null + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/StyleableOverlaySource.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/StyleableOverlaySource.kt new file mode 100644 index 00000000000..3d139f31b55 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/map/sources/StyleableOverlaySource.kt @@ -0,0 +1,132 @@ +package de.westnordost.streetcomplete.screens.main.map.sources + +import de.westnordost.streetcomplete.data.download.tiles.TilesRect +import de.westnordost.streetcomplete.data.download.tiles.enclosingTilesRect +import de.westnordost.streetcomplete.data.osm.edits.MapDataWithEditsSource +import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox +import de.westnordost.streetcomplete.data.osm.mapdata.ElementKey +import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.key +import de.westnordost.streetcomplete.data.overlays.Overlay +import de.westnordost.streetcomplete.data.overlays.SelectedOverlaySource +import de.westnordost.streetcomplete.screens.main.map.layers.StyledElement +import de.westnordost.streetcomplete.screens.main.map.layers.isDisabled +import de.westnordost.streetcomplete.screens.main.map.layers.toElementKey +import de.westnordost.streetcomplete.util.math.intersect +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject + +class StyleableOverlaySource( + private val selectedOverlaySource: SelectedOverlaySource, + private val mapDataWithEditsSource: MapDataWithEditsSource, +) { + private val displayedRect = MutableStateFlow(null) + + val styledElements: Flow> = channelFlow { + displayedRect.collectLatest { rect -> + if (rect == null) { + send(emptyList()) + return@collectLatest + } + val bbox = rect.asBoundingBox(TILES_ZOOM) + val elements = mutableMapOf() + var overlay: Overlay? = null + events().collect { event -> + when (event) { + Event.Reload -> { + val (selectedOverlay, data) = withContext(Dispatchers.IO) { + val selected = selectedOverlaySource.selectedOverlay + selected to selected?.let { + createStyledElementsByKey(it, mapDataWithEditsSource.getMapDataWithGeometry(bbox)) + .toMap() + } + } + overlay = selectedOverlay + elements.clear() + if (data != null) elements.putAll(data) + } + Event.Clear -> elements.clear() + is Event.Updated -> { + event.deleted.forEach { elements.remove(it) } + event.updated.forEach { elements.remove(it.key) } + overlay?.let { selected -> + createStyledElementsByKey(selected, event.updated).forEach { (key, element) -> + if (bbox.intersect(element.geometry.bounds)) elements[key] = element + } + } + } + } + send(elements.values.toList()) + } + } + }.flowOn(Dispatchers.Default) + + private fun events(): Flow = callbackFlow { + val overlayListener = object : SelectedOverlaySource.Listener { + override fun onSelectedOverlayChanged() { trySend(Event.Reload) } + } + val dataListener = object : MapDataWithEditsSource.Listener { + override fun onUpdated(updated: MapDataWithGeometry, deleted: Collection) { + trySend(Event.Updated(updated, deleted.toList())) + } + override fun onReplacedForBBox(bbox: BoundingBox, mapDataWithGeometry: MapDataWithGeometry) { + trySend(Event.Reload) + } + override fun onCleared() { trySend(Event.Clear) } + } + selectedOverlaySource.addListener(overlayListener) + mapDataWithEditsSource.addListener(dataListener) + trySend(Event.Reload) + awaitClose { + selectedOverlaySource.removeListener(overlayListener) + mapDataWithEditsSource.removeListener(dataListener) + } + }.buffer(Channel.UNLIMITED) + + fun onMapMoved(zoom: Double, displayedArea: BoundingBox?) { + if (displayedArea == null) { + displayedRect.value = null + return + } + // Keep the loaded data when zooming out. + if (zoom < MIN_ZOOM) return + val rect = displayedArea.enclosingTilesRect(TILES_ZOOM) + if (rect.size > 32) return + if (displayedRect.value?.contains(rect) != true) displayedRect.value = rect + } + + private fun createStyledElementsByKey( + overlay: Overlay, + mapData: MapDataWithGeometry + ): Sequence> = + overlay.getStyledElements(mapData).mapNotNull { (element, style) -> + val key = element.key + val geometry = mapData.getGeometry(element.type, element.id) ?: return@mapNotNull null + key to StyledElement(element, geometry, style) + } + + private sealed interface Event { + data object Reload : Event + data object Clear : Event + data class Updated(val updated: MapDataWithGeometry, val deleted: List) : Event + } + + fun getElementKey(properties: JsonObject): ElementKey? = + if (!properties.isDisabled()) properties.toElementKey() else null + + companion object { + private const val TILES_ZOOM = 16 + private const val MIN_ZOOM = 14 + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/tutorial/IntroTutorialScreen.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/tutorial/IntroTutorialScreen.kt index 3d3106a3ae9..181e7740160 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/tutorial/IntroTutorialScreen.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/tutorial/IntroTutorialScreen.kt @@ -30,10 +30,10 @@ import de.westnordost.streetcomplete.screens.main.controls.LocationStateButton import de.westnordost.streetcomplete.ui.common.Pin import de.westnordost.streetcomplete.ui.theme.headlineLarge import de.westnordost.streetcomplete.ui.theme.titleLarge -import de.westnordost.streetcomplete.ui.util.rememberDefaultLocationProvider import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.maplibre.compose.location.rememberDefaultLocationProvider /** Shows a short tutorial for first-time users */ @Composable diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/Pin.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/Pin.kt index 6b265ccd790..b3124295baa 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/Pin.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/Pin.kt @@ -1,19 +1,15 @@ package de.westnordost.streetcomplete.ui.common import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.resources.* +import de.westnordost.streetcomplete.screens.main.map.pinPainter import org.jetbrains.compose.resources.painterResource /** A pin with an image on the pin head, usually a quest icon. The point the pin is pointing at is @@ -23,32 +19,13 @@ fun Pin( iconPainter: Painter, modifier: Modifier = Modifier ) { - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { - Box(modifier) { - Image( - painter = painterResource(Res.drawable.pin_shadow), - contentDescription = null, - modifier = Modifier - .padding(end = 10.dp, bottom = 71.dp) - .size(71.dp, 71.dp) - ) - Image( - painter = painterResource(Res.drawable.pin), - contentDescription = null, - modifier = Modifier - .padding(top = 5.dp) - .align(Alignment.TopCenter) - ) - Image( - painter = iconPainter, - contentDescription = null, - modifier = Modifier - .padding(top = 7.dp) - .size(48.dp) - .align(Alignment.TopCenter) - ) - } - } + Image( + painter = pinPainter(iconPainter), + contentDescription = null, + modifier = modifier + .padding(end = 10.dp, bottom = 71.dp) + .size(71.dp) + ) } @Composable diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/ktx/Res.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/ktx/Res.kt index 3b863ad2703..075c3eca740 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/ktx/Res.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/ktx/Res.kt @@ -2,9 +2,11 @@ package de.westnordost.streetcomplete.ui.ktx import com.charleskorn.kaml.Yaml import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.allDrawableResources import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString +import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.MissingResourceException expect fun Res.exists(path: String): Boolean @@ -19,3 +21,9 @@ suspend inline fun Res.readYamlOrNull(path: String, yaml: Yaml = Yam suspend fun Res.readBytesOrNull(path: String): ByteArray? = try { readBytes(path) } catch (_: MissingResourceException) { null } + +private val resourceIds by lazy { + Res.allDrawableResources.entries.associate { (id, resource) -> resource to id } +} + +val DrawableResource.id: String? get() = resourceIds.get(this) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt index c5b3e1cf20d..33861d05463 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt @@ -45,6 +45,10 @@ val Color.Companion.GrassGreen get() = Color(0xff80b158) val Color.Companion.GrassGray get() = Color(0xff888888) val Color.Companion.LeafGreen get() = Color(0xff006a00) +val Color.Companion.Location get() = Color(0xff536dfe) +val Color.Companion.GeometryMarker get() = Color(0xffD140D0) +val Color.Companion.Recording get() = Color(0xfffe1616) + val LightColors = lightColors( primary = Color(0xff4141ba), primaryVariant = Color(0xff3939a3), diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/ColorFilterPainter.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/ColorFilterPainter.kt new file mode 100644 index 00000000000..48059f1cad1 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/ColorFilterPainter.kt @@ -0,0 +1,22 @@ +package de.westnordost.streetcomplete.ui.util + +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter + +/** Painter that paints the given [painter] with the given [colorFilter] and optionally a custom + * [alpha]. */ +class ColorFilterPainter( + private val painter: Painter, + private val colorFilter: ColorFilter, + private val alpha: Float = 1.0f, +) : Painter() { + + override val intrinsicSize = painter.intrinsicSize + + override fun DrawScope.onDraw() { + with(painter) { + draw(size, alpha = alpha, colorFilter = colorFilter) + } + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.kt new file mode 100644 index 00000000000..8401ab3ded7 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.kt @@ -0,0 +1,25 @@ +package de.westnordost.streetcomplete.ui.util + +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Shader + +expect fun createDilateShader(inputImage: ImageBitmap, radius: Float, color: Color, canvas: Canvas): Shader? + +internal fun createDilateSksl(radius: Float) = """ +uniform shader inputImage; +uniform half4 dilationColor; + +half4 main(float2 coord) { + float maxAlpha = 0.0; + for (float x = -$radius; x <= $radius; x += 1.0) { + for (float y = -$radius; y <= $radius; y += 1.0) { + float2 sampleCoord = coord + float2(x, y); + half4 color = inputImage.eval(sampleCoord); + maxAlpha = max(maxAlpha, color.a); + } + } + return half4(dilationColor.rgb, dilationColor.a * maxAlpha); +} +""" diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.kt deleted file mode 100644 index 71acd19a93d..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.westnordost.streetcomplete.ui.util - -import androidx.compose.runtime.Composable -import org.maplibre.compose.location.LocationProvider - -// TODO maplibre-compose: can be deleted once map is based on maplibre-compose -@Composable -public expect fun rememberDefaultLocationProvider(): LocationProvider diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/WithHaloPainter.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/WithHaloPainter.kt new file mode 100644 index 00000000000..43a50513bdc --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/WithHaloPainter.kt @@ -0,0 +1,67 @@ +package de.westnordost.streetcomplete.ui.util + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.drawscope.CanvasDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.inset +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +/** Painter that draws an outline (a.k.a. halo) of given [outlineWidth] and [outlineColor] around + * opaque parts of the given [painter] */ +class WithHaloPainter( + val painter: Painter, + val density: Density, + val outlineWidth: Dp = 1.dp, + val outlineColor: Color = Color.White, +) : Painter() { + override val intrinsicSize: Size + get() { + val padding = outlineWidth.value * density.density + return Size( + width = painter.intrinsicSize.width + padding * 2, + height = painter.intrinsicSize.height + padding * 2 + ) + } + + override fun DrawScope.onDraw() { + val padding = outlineWidth.value * density + val alphaImage = createAlphaImageBitmap(size, Density(density), layoutDirection) { + inset(padding) { + with(painter) { draw(size) } + } + } + val shader = createDilateShader(alphaImage, padding, outlineColor, drawContext.canvas) + if (shader != null) drawRect(ShaderBrush(shader)) + + inset(padding) { + with(painter) { draw(size) } + } + } +} + +/** Create a bitmap containing only the alpha channel from whatever is drawn in the [block] */ +private fun createAlphaImageBitmap( + size: Size, + density: Density, + layoutDirection: LayoutDirection, + block: DrawScope.() -> Unit +): ImageBitmap { + val imageBitmap = ImageBitmap( + width = size.width.toInt(), + height = size.height.toInt(), + config = ImageBitmapConfig.Alpha8 + ) + val canvas = Canvas(imageBitmap) + val drawScope = CanvasDrawScope() + drawScope.draw(density, layoutDirection, canvas, size, block) + return imageBitmap +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/sdf/Sdf.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/sdf/Sdf.kt deleted file mode 100644 index c05c4dd4626..00000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/sdf/Sdf.kt +++ /dev/null @@ -1,91 +0,0 @@ -package de.westnordost.streetcomplete.util.sdf - -import kotlin.math.max -import kotlin.math.round -import kotlin.math.sqrt - -// initially copied from https://github.com/mapbox/tiny-sdf/blob/main/index.js and then made to -// work with a simple ARGB IntArray and simplified - -private const val INF = 1e20 - -fun convertToSdf( - bitmap: IntArray, - width: Int, - radius: Double = 8.0, - cutoff: Double = 0.25 -) { - val height = bitmap.size / width - - val gridOuter = DoubleArray(bitmap.size) { i -> - when (val a = bitmap[i].ushr(24)) { - 0 -> INF - 255 -> 0.0 - else -> { - val d = 0.5f - (a.toDouble() / 255.0) - if (d > 0) d * d else 0.0 - } - } - } - - val gridInner = DoubleArray(bitmap.size) { i -> - when (val a = bitmap[i].ushr(24)) { - 0 -> 0.0 - 255 -> INF - else -> { - val d = 0.5f - (a.toDouble() / 255.0) - if (d < 0) d * d else 0.0 - } - } - } - val size = max(width, height) - val f = DoubleArray(size) - val z = DoubleArray(size + 1) - val v = IntArray(size) - - edt(gridOuter, width, height, f, v, z) - edt(gridInner, width, height, f, v, z) - - for (i in bitmap.indices) { - val dist = sqrt(gridOuter[i]) - sqrt(gridInner[i]) - bitmap[i] = round(255 - 255 * (dist / radius + cutoff)).toInt().coerceIn(0, 255).shl(24) - } -} - -// 2D Euclidean squared distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/papers/dt-final.pdf -private fun edt(data: DoubleArray, width: Int, height: Int, f: DoubleArray, v: IntArray, z: DoubleArray) { - for (x in 0.. -1) - - k++ - v[k] = q - z[k] = s - z[k + 1] = INF - } - - k = 0 - for (q in 0.. { IosLocationProvider() } + single { IosLocationProvider() } onClose { it?.close() } + factory { IosHeadingProvider() } factory { IosSystemSettingsLauncher() } // settings @@ -131,6 +134,24 @@ val iosModule = module { factory { IosActiveNetworkConnection() } + // map + + single { + val appSupportUrl = NSFileManager.defaultManager.URLForDirectory( + directory = NSApplicationSupportDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = true, + error = null + )!! + val cacheFile = Path(appSupportUrl.path!!, "maplibre-cache.db") + createMapRuntime(MapRuntimeOptions(cacheFile = cacheFile)) + } onClose { it?.close() } + + factory { + MapLibreMapTilesDownloader(get().offlineManager, UIScreen.mainScreen.scale.toFloat()) + } + // background jobs single { IosUploadController() } @@ -140,6 +161,4 @@ val iosModule = module { factory { IosChangesetAutoCloser() } factory { IosPeriodicCleaner() } - - factory { IosMapTilesDownloader() } } diff --git a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/data/maptiles/IosMapTilesDownloader.kt b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/data/maptiles/IosMapTilesDownloader.kt deleted file mode 100644 index f79ee1ec700..00000000000 --- a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/data/maptiles/IosMapTilesDownloader.kt +++ /dev/null @@ -1,9 +0,0 @@ -package de.westnordost.streetcomplete.data.maptiles - -import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox - -class IosMapTilesDownloader : MapTilesDownloader { - override suspend fun download(bbox: BoundingBox) {} - - override suspend fun clear() {} -} diff --git a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.ios.kt b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.ios.kt deleted file mode 100644 index 6dd7f2fc116..00000000000 --- a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ScaleBarMeasure.ios.kt +++ /dev/null @@ -1,6 +0,0 @@ -package de.westnordost.streetcomplete.screens.main.controls - -import androidx.compose.runtime.Composable - -@Composable -internal actual fun systemDefaultPrimaryMeasure(): ScaleBarMeasure? = null diff --git a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.ios.kt b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.ios.kt new file mode 100644 index 00000000000..4d56c825cd8 --- /dev/null +++ b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/DilateShader.ios.kt @@ -0,0 +1,19 @@ +package de.westnordost.streetcomplete.ui.util + +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.asComposeShader +import androidx.compose.ui.graphics.asSkiaBitmap +import org.jetbrains.skia.RuntimeEffect +import org.jetbrains.skia.RuntimeShaderBuilder + +actual fun createDilateShader(inputImage: ImageBitmap, radius: Float, color: Color, canvas: Canvas): Shader? { + val effect = RuntimeEffect.makeForShader(createDilateSksl(radius)) + val shaderBuilder = RuntimeShaderBuilder(effect).apply { + child("inputImage", inputImage.asSkiaBitmap().makeShader()) + uniform("dilationColor", color.red, color.green, color.blue, color.alpha) + } + return shaderBuilder.makeShader().asComposeShader() +} diff --git a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.ios.kt b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.ios.kt deleted file mode 100644 index c26c1f6e622..00000000000 --- a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.ios.kt +++ /dev/null @@ -1,14 +0,0 @@ -package de.westnordost.streetcomplete.ui.util - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember -import org.maplibre.compose.location.IosLocationProvider -import org.maplibre.compose.location.LocationProvider - -// Identical to the upstream maplibre-compose implementation. -@Composable actual fun rememberDefaultLocationProvider(): LocationProvider { - val provider = remember { IosLocationProvider() } - DisposableEffect(provider) { onDispose { provider.close() } } - return provider -} diff --git a/build.gradle.kts b/build.gradle.kts index 3542501ee54..9ab4c3fb9f1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -87,7 +87,6 @@ tasks.register("updateStreetCompleteData") { "app:updateTranslations", "app:updateTranslationCompleteness", "app:updateIosTranslations", - "app:updateMapStyle", "app:updateChangelog", "app:generateMetadataByCountry", "app:updateTranslatorCredits", diff --git a/buildSrc/src/main/java/CopyIconsTask.kt b/buildSrc/src/main/java/CopyIconsTask.kt deleted file mode 100644 index 92f79f715b8..00000000000 --- a/buildSrc/src/main/java/CopyIconsTask.kt +++ /dev/null @@ -1,53 +0,0 @@ -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction -import java.io.File - -open class CopyIconsTask : DefaultTask() { - @get:InputDirectory lateinit var sourceDir: File - @get:OutputDirectory lateinit var targetDir: File - @get:Input lateinit var filter: (String) -> Boolean - @get:OutputFile lateinit var indexFile: File - - @TaskAction - fun run() { - val fileNames = ArrayList() - val dimensions = listOf("", "-mdpi", "-hdpi", "-xhdpi", "-xxhdpi") - val sourceDirs = dimensions.map { File(sourceDir.path + it) } - val targetDirs = dimensions.map { File(targetDir.path + it) } - - for (i in dimensions.indices) { - val srcDir = sourceDirs[i] - val trgDir = targetDirs[i] - for (file in srcDir.listFiles().orEmpty()) { - if (file.isFile) { - if (filter(file.name)) { - file.copyTo(trgDir.resolve( file.name), overwrite = true) - fileNames.add(file.name.substringBeforeLast('.')) - } - } - } - } - fileNames.sort() - - indexFile.parentFile.mkdirs() - indexFile.writeText(""" - package de.westnordost.streetcomplete.view - - import de.westnordost.streetcomplete.R - import de.westnordost.streetcomplete.resources.* - import org.jetbrains.compose.resources.DrawableResource - - fun DrawableResource.toAndroidResourceId(): Int? = icons[this] - - private val icons = mapOf( - ${fileNames.joinToString(separator = ",\n ") { fileName -> - "Res.drawable.$fileName to R.drawable.$fileName" - }} - ) - """.trimIndent()) - } -} diff --git a/buildSrc/src/main/java/CopyStringsTask.kt b/buildSrc/src/main/java/CopyStringsTask.kt deleted file mode 100644 index 4e187eb1546..00000000000 --- a/buildSrc/src/main/java/CopyStringsTask.kt +++ /dev/null @@ -1,38 +0,0 @@ -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskAction -import java.io.File -import kotlin.collections.orEmpty - -open class CopyStringsTask : DefaultTask() { - @get:InputDirectory lateinit var sourceDir: File - @get:OutputDirectory lateinit var targetDir: File - - @TaskAction - fun run() { - val stringRegex = Regex( - pattern = "(.*?)", - options = setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.MULTILINE) - ) - for (dir in sourceDir.listFiles().orEmpty()) { - if (dir.isDirectory) { - val stringsFile = File(dir, "strings.xml") - if (stringsFile.exists()) { - val stringsFileText = stringsFile.readText() - val newStringsFileText = stringRegex.replace(stringsFileText) { - val key = it.groupValues[1] - val value = it.groupValues[2] - "\"${value.escapeXml().replace("\"", "\\\"")}\"" - } - - val targetStringsDir = targetDir.resolve(dir.name) - val targetStringsFile = targetStringsDir.resolve(stringsFile.name) - - targetStringsDir.mkdirs() - targetStringsFile.writeText(newStringsFileText) - } - } - } - } -} diff --git a/buildSrc/src/main/java/UpdateMapStyleTask.kt b/buildSrc/src/main/java/UpdateMapStyleTask.kt deleted file mode 100644 index 05780ada18a..00000000000 --- a/buildSrc/src/main/java/UpdateMapStyleTask.kt +++ /dev/null @@ -1,47 +0,0 @@ -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskAction -import java.io.File -import java.net.URI - -/** Pulls the newest map style from the maplibre-streetcomplete-style repository */ -open class UpdateMapStyleTask : DefaultTask() { - - @get:OutputDirectory lateinit var targetDir: File - @get:Input lateinit var mapStyleBranch: String - @get:Input lateinit var apiKey: String - - @TaskAction fun run() { - require(targetDir.exists()) { "Directory ${targetDir.absolutePath} does not exist." } - - val urls = listOf( - "https://raw.githubusercontent.com/streetcomplete/maplibre-streetcomplete-style/master/demo/streetcomplete.json", - "https://raw.githubusercontent.com/streetcomplete/maplibre-streetcomplete-style/master/demo/streetcomplete-night.json", - ).map { URI(it).toURL() } - - for (url in urls) { - val fileName = File(url.path).name - val targetFile = targetDir.resolve(fileName) - - val fileContent = url.readText() - .normalizeLineEndings() - .replaceAccessToken(apiKey) - .replaceGlyphs() - .replaceSprites() - - targetFile.writeText(fileContent) - } - } -} - -private fun String.normalizeLineEndings() = this.replace("\r\n", "\n") - -private fun String.replaceAccessToken(apiKey: String): String = - replace(Regex("\\?access-token=[0-9A-Za-z+/=]*"), "?access-token=$apiKey") - -private fun String.replaceGlyphs(): String = - replace(Regex("https://api.jawg.io/glyphs"), "asset://map_theme/glyphs") - -private fun String.replaceSprites(): String = - replace(Regex("https://streetcomplete.app/map-jawg/sprites"), "asset://map_theme/sprites") diff --git a/docs/maplibre-compose-migration.md b/docs/maplibre-compose-migration.md new file mode 100644 index 00000000000..d7f626146f9 --- /dev/null +++ b/docs/maplibre-compose-migration.md @@ -0,0 +1,79 @@ +# Remaining MapLibre Compose migration work + +Finish the production MapLibre Compose migration on top of +[StreetComplete #6352](https://github.com/streetcomplete/StreetComplete/pull/6352), +preserving Android behavior and keeping the map implementation shared for future +iOS work. Full iOS UI/platform-service migration, desktop support, and unrelated +activity/navigation cleanup are outside this plan. + +[#7068](https://github.com/streetcomplete/StreetComplete/pull/7068) +is a reference for individual migration implementations, not additional scope. + +## Maintaining this plan + +- This document contains remaining work, not a progress log. +- Delete a defect, task, or acceptance item when its implementation and relevant + validation are complete. For partially completed work, remove the completed + portion and retain the remaining requirement. +- Delete empty sections. Do not retain checked boxes, completed sections, + resolutions, dated updates, or a history of previous approaches. +- Add entries only when a new defect or gap is discovered. Include the affected + behavior, source or reproducer, and the evidence needed to close it. Distinguish + confirmed defects from behavior that still needs investigation or validation. +- Refine existing entries in place when evidence changes. Keep implementation + and validation evidence in tests, commits, and PR descriptions rather than + appending it here. A code change alone does not close a device-validation gap. +- Delete this document when no migration work remains. Put any lasting maintenance + instructions in the appropriate existing project documentation. + +## Upstream releases still required + +- **Offline completion and initial pack loading:** adopt a release containing + [MapLibre Compose #1405](https://github.com/maplibre/maplibre-compose/pull/1405). + Collect `downloadProgress` directly and read `packs.value` after the manager's + initial load. In v0.16, headless `snapshotFlow` can remain suspended after a + download finishes, and cold-start cleanup can miss saved packs. Verify the + download worker completes without an activity and that `Cleaner.cleanOld` and + `cleanAll` include existing packs after a cold start. +- **Focus and cluster camera parity:** adopt `cameraForBounds` from a release + containing [#1400](https://github.com/maplibre/maplibre-compose/pull/1400). + Restore the 0.75 focus / 0.25 cluster zoom margins, maximum zoom 19, + zoom-dependent durations, and the 0.5 focus zoom threshold. Check point, line, + and polygon focus and dense clusters with sheet padding, bearing, and tilt. +- **Android shader halos:** adopt a release containing + [#1394](https://github.com/maplibre/maplibre-compose/pull/1394), then verify pin + halos on API 33+ and keep the no-halo fallback below API 33. +- **Permission changes:** remove `updatesWithPermissionChanges` after adopting a + release containing [#1393](https://github.com/maplibre/maplibre-compose/pull/1393). + Verify deny/grant/revoke and return from settings during foreground tracking. +- **Style replacement:** remove the background/road layer ID workaround when the + runtime includes the fix for + [native-ffi #709](https://github.com/maplibre/maplibre-native-ffi/issues/709). + Verify light/dark style changes and overlay replacement preserve their colors. + +## Production validation still required + +- Compare the shared map with the legacy map for layer order, road/bridge + overlays, multipolygons/holes, pin collision padding, selected-pin animation, + location/track synchronization, labels, font scale, language, theme, and system + animation settings. Fix unintended differences; validate StreetComplete behavior + with checks proportional to the existing tests. Do not require pixel parity for + deliberate changes such as removal of enlarged hit areas. +- Exercise the Android production UI on a physical device with dense quests and + clusters, edit history, overlay/form/create modes, GPS/navigation/recording, + airplane-mode use after download and restart, download cancellation/deletion, + background/resume, and activity/process recreation. Verify release packaging + includes the required resources and native libraries for supported Android ABIs. +- Build the iOS host and exercise the shared map with changing data, registered + images, gestures, style replacement, and background/resume. Verify painter + dimensions and glyph loading. This validates the shared map, not the full iOS + application migration. +- **Needs investigation: map-entry responsiveness on the Moto G 5G (2024).** + The former debug map entry reportedly froze the UI for about a second. Profile + cold startup and repeated entry to the production map, distinguish UI-thread + blocking from renderer initialization and tile loading, and verify navigation + stays responsive. +- Validate the selected Vulkan backend on supported Android devices, including + an older device below API 33; the API 26 emulator cannot initialize Vulkan. + Check map-entry responsiveness, rendering correctness, and background/resume. Resolve + demonstrated compatibility problems before production cutover. diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index e7526fca45b..506e0edf748 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -313,6 +313,24 @@ "$(inherited)", "@executable_path/Frameworks", ); + OTHER_LDFLAGS = ( + "-l\"c++\"", + "-lz", + "-framework", + CoreFoundation, + "-framework", + CoreGraphics, + "-framework", + CoreText, + "-framework", + Foundation, + "-framework", + ImageIO, + "-framework", + Metal, + "-framework", + QuartzCore, + ); SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -346,6 +364,24 @@ "$(inherited)", "@executable_path/Frameworks", ); + OTHER_LDFLAGS = ( + "-l\"c++\"", + "-lz", + "-framework", + CoreFoundation, + "-framework", + CoreGraphics, + "-framework", + CoreText, + "-framework", + Foundation, + "-framework", + ImageIO, + "-framework", + Metal, + "-framework", + QuartzCore, + ); SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/res/graphics/map/location_shadow.svg b/res/graphics/map/location_shadow.svg new file mode 100644 index 00000000000..8e1ab31f3a1 --- /dev/null +++ b/res/graphics/map/location_shadow.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/graphics/map/map_oneway_arrow.svg b/res/graphics/map/map_oneway_arrow.svg new file mode 100644 index 00000000000..d1120ab1a25 --- /dev/null +++ b/res/graphics/map/map_oneway_arrow.svg @@ -0,0 +1,3 @@ + + +