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