aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2022-01-16 04:16:53 -0600
committerIván Ávalos <avalos@disroot.org>2022-01-16 04:16:53 -0600
commit4a796e47bede0cbe5512868001c22b6889e9f72e (patch)
tree48aa1dfed3dcae88f0453c1c9c4788d04173f9e0
parent51d42034f00cc640ff94c06333e2354c8e440a3f (diff)
parenta2ba612c515a53cb6e7f283858a518d60188651b (diff)
downloadetbsa-trackermap-mobile-4a796e47bede0cbe5512868001c22b6889e9f72e.tar.gz
etbsa-trackermap-mobile-4a796e47bede0cbe5512868001c22b6889e9f72e.tar.bz2
etbsa-trackermap-mobile-4a796e47bede0cbe5512868001c22b6889e9f72e.zip
Merge branch 'main' of https://git.sr.ht/~avalos/trackermap-mobile
-rw-r--r--androidApp/build.gradle.kts3
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt85
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/UnitMapFragment.kt11
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt12
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt12
-rw-r--r--androidApp/src/main/res/layout/unit_map_fragment.xml23
-rw-r--r--androidApp/src/main/res/values-es-rMX/map_layers.xml5
-rw-r--r--androidApp/src/main/res/values-es-rMX/strings.xml3
-rw-r--r--androidApp/src/main/res/values/colors.xml2
-rw-r--r--androidApp/src/main/res/values/dimen.xml1
-rw-r--r--androidApp/src/main/res/values/map_layers.xml26
-rw-r--r--androidApp/src/main/res/values/strings.xml11
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/MapLayer.kt8
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/MapCalculus.kt37
14 files changed, 190 insertions, 49 deletions
diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
index 6fe79ad..bec9466 100644
--- a/androidApp/build.gradle.kts
+++ b/androidApp/build.gradle.kts
@@ -39,7 +39,7 @@ val googleImplementation by configurations
dependencies {
implementation(project(":shared"))
- implementation("com.google.android.material:material:1.4.0")
+ implementation("com.google.android.material:material:1.5.0")
implementation("androidx.appcompat:appcompat:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.2")
implementation("com.squareup.okhttp3:okhttp:4.9.1")
@@ -56,6 +56,7 @@ dependencies {
implementation("com.github.zerobranch:SwipeLayout:1.3.1")
implementation("com.github.addisonElliott:SegmentedButton:3.1.9")
implementation("mil.nga.sf:sf-wkt:1.0.1")
+ implementation("com.soywiz.korlibs.krypto:krypto:2.4.12")
implementation(group = "", name = "WhirlyGlobeMaply", ext = "aar")
googleImplementation(platform("com.google.firebase:firebase-bom:29.0.3"))
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt
index 60c8a47..a8478fd 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt
@@ -11,12 +11,15 @@ import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import com.mousebird.maply.*
+import com.soywiz.krypto.md5
import mil.nga.sf.Polygon
import mil.nga.sf.util.SFException
import mil.nga.sf.wkt.GeometryReader
import java.io.File
import mx.trackermap.TrackerMap.android.R
import mx.trackermap.TrackerMap.client.models.Geofence
+import mx.trackermap.TrackerMap.client.models.MapLayer
+import mx.trackermap.TrackerMap.utils.MapCalculus
import mx.trackermap.TrackerMap.utils.MarkerType
typealias MarkerCallback = (Int?) -> Unit
@@ -56,17 +59,14 @@ class MapFragment : GlobeMapFragment() {
override fun controlHasStarted() {
Log.d("MapFragment", "controlHasStarted")
- val cacheDirName = "stamen_watercolor6"
- val cacheDir = File(activity!!.cacheDir, cacheDirName)
- cacheDir.mkdir()
-
if (tileInfo == null) {
- tileInfo = RemoteTileInfoNew(
- getString(R.string.maps_streets_tile_url),
- 0,
- 21
- )
- (tileInfo as RemoteTileInfoNew).cacheDir = cacheDir
+ // Load default map layer
+ val layer = resources.getStringArray(R.array.maps_streets_tile)
+ val tmpInfo = RemoteTileInfoNew(layer[0], layer[1].toInt(), layer[2].toInt())
+ tileInfo = tileInfoSetCacheDir(layer[0], tmpInfo)
+ tileInfo?.let {
+ setZoomLimits(it.minZoom, it.maxZoom)
+ }
}
val params = SamplingParams()
@@ -83,7 +83,16 @@ class MapFragment : GlobeMapFragment() {
val latitude = 23.191
val longitude = -100.36
- focusOn(latitude, longitude, zoom = 0.4, animated = false)
+ focusOn(latitude, longitude, height = 0.4, animated = false)
+ }
+
+ override fun mapDidStopMoving(
+ mapControl: MapController?,
+ corners: Array<out Point3d>?,
+ userMotion: Boolean
+ ) {
+ super.mapDidStopMoving(mapControl, corners, userMotion)
+ Log.d("MapFragment", "Height: ${mapControl?.height}")
}
override fun userDidSelect(
@@ -93,7 +102,6 @@ class MapFragment : GlobeMapFragment() {
screenLoc: Point2d?
) {
super.userDidSelect(mapControl, selObjs, loc, screenLoc)
-
selObjs?.forEach { selectedObject ->
if (selectedObject.selObj is ScreenMarker) {
val screenMarker = selectedObject.selObj as ScreenMarker
@@ -228,7 +236,8 @@ class MapFragment : GlobeMapFragment() {
mbr.expandByFraction(0.1)
mapControl.addPostSurfaceRunnable {
- val zoom = mapControl.findHeightToViewBounds(mbr, mbr.middle())
+ val zoom = mapControl.zoomLimitMax.coerceAtLeast(
+ mapControl.findHeightToViewBounds(mbr, mbr.middle()))
mapControl.setPositionGeo(mbr.middle(), zoom)
}
}
@@ -308,19 +317,57 @@ class MapFragment : GlobeMapFragment() {
))
}
- fun focusOn(latitude: Double, longitude: Double, zoom: Double = 0.0000144, animated: Boolean = true) {
+ fun focusOn(
+ latitude: Double,
+ longitude: Double,
+ height: Double? = 0.00001,
+ animated: Boolean = true
+ ) {
val lat = latitude * Math.PI / 180
val lon = longitude * Math.PI / 180
+ // Ensure height is equal or higher than bottom limit
+ val z = mapControl.zoomLimitMin.coerceAtLeast(height ?: 0.0)
if (animated) {
- mapControl.animatePositionGeo(lon, lat, zoom, 0.2)
+ mapControl.animatePositionGeo(lon, lat, z, 0.2)
} else {
- mapControl.setPositionGeo(lon, lat, zoom)
+ mapControl.setPositionGeo(lon, lat, z)
}
}
- fun updateTileInfo(tileInfo: TileInfoNew) {
- this.tileInfo = tileInfo
- loader?.changeTileInfo(tileInfo)
+ private fun tileInfoSetCacheDir(url: String, tileInfo: TileInfoNew): TileInfoNew? {
+ return context?.let {
+ val cacheDirName = url.toByteArray(Charsets.UTF_8).md5().hex
+ val cacheDirMap = File(it.cacheDir, cacheDirName)
+ cacheDirMap.mkdir()
+ Log.d("MapFragment", "Cache dir for $url = ${cacheDirMap.absolutePath}")
+
+ (tileInfo as? RemoteTileInfoNew)?.cacheDir = cacheDirMap
+ tileInfo
+ }
+ }
+
+ fun updateTile(layer: MapLayer) {
+ val tileInfo = RemoteTileInfoNew(layer.url, layer.minZoom, layer.maxZoom)
+ this.tileInfo = tileInfoSetCacheDir(layer.url, tileInfo)
+ this.tileInfo?.let {
+ loader?.changeTileInfo(it)
+ setZoomLimits(tileInfo.minZoom, tileInfo.maxZoom)
+ }
+ }
+
+ private fun setZoomLimits(minZoom: Int, maxZoom: Int) {
+ mapControl?.let {
+ it.setZoomLimits(
+ it.heightForMapScale(
+ MapCalculus.zoomLevelToScale(maxZoom)
+ ?: MapCalculus.zoomLevelToScale(21)!!
+ ),
+ it.heightForMapScale(
+ MapCalculus.zoomLevelToScale(minZoom)
+ ?: MapCalculus.zoomLevelToScale(1)!!
+ )
+ )
+ }
}
private fun getIcon(markerType: MarkerType): Bitmap {
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/UnitMapFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/UnitMapFragment.kt
index 09f41a8..6f6596f 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/UnitMapFragment.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/UnitMapFragment.kt
@@ -6,6 +6,7 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.text.HtmlCompat
import kotlinx.coroutines.DelicateCoroutinesApi
import mx.trackermap.TrackerMap.android.R
import mx.trackermap.TrackerMap.android.databinding.UnitMapFragmentBinding
@@ -53,6 +54,9 @@ class UnitMapFragment(private val unitsViewModel: UnitsViewModel) : UnitFragment
private fun initializeMap() {
unitsMapFragment = childFragmentManager.findFragmentById(R.id.unitsMap) as MapFragment
unitsMapFragment.markerCallback = unitsViewModel::selectUnitWith
+
+ val layer = resources.getStringArray(R.array.maps_streets_tile)
+ binding.attributionText.text = HtmlCompat.fromHtml(layer[3], 0)
}
private fun setupObservers() {
@@ -94,9 +98,10 @@ class UnitMapFragment(private val unitsViewModel: UnitsViewModel) : UnitFragment
}
}
- unitsViewModel.mapTileInfo.observe(viewLifecycleOwner) { tileInfo ->
+ unitsViewModel.mapLayer.observe(viewLifecycleOwner) { layer ->
Log.d("UnitMapFragment", "Loading layer!")
- unitsMapFragment.updateTileInfo(tileInfo)
+ unitsMapFragment.updateTile(layer)
+ binding.attributionText.text = HtmlCompat.fromHtml(layer.attribution, 0)
}
unitsViewModel.geofences.observe(viewLifecycleOwner) { geofences ->
@@ -108,7 +113,7 @@ class UnitMapFragment(private val unitsViewModel: UnitsViewModel) : UnitFragment
Log.d("UnitMapFragment", "removeObservers()")
unitsViewModel.units.removeObservers(viewLifecycleOwner)
unitsViewModel.selectedUnit.removeObservers(viewLifecycleOwner)
- unitsViewModel.mapTileInfo.removeObservers(viewLifecycleOwner)
+ unitsViewModel.mapLayer.removeObservers(viewLifecycleOwner)
unitsViewModel.geofences.removeObservers(viewLifecycleOwner)
}
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt
index bf5f1d8..1bbcb80 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt
@@ -17,6 +17,7 @@ import mx.trackermap.TrackerMap.android.databinding.UnitsActivityBinding
import mx.trackermap.TrackerMap.android.devices.DevicesFragment
import mx.trackermap.TrackerMap.android.map.UnitMapFragment
import mx.trackermap.TrackerMap.android.session.UserInformationActivity
+import mx.trackermap.TrackerMap.client.models.MapLayer
import org.koin.androidx.viewmodel.ext.android.viewModel
@DelicateCoroutinesApi
@@ -90,13 +91,16 @@ class UnitsActivity : AppCompatActivity() {
val popOver = PopupMenu(this, view)
popOver.menuInflater.inflate(R.menu.map_layers, popOver.menu)
popOver.setOnMenuItemClickListener { item ->
- unitsViewModel.setMapLayer(
+ val layer = resources.getStringArray(
when (item.itemId) {
- R.id.layerStreets -> getString(R.string.maps_streets_tile_url)
- R.id.layerSatellite -> getString(R.string.maps_satellite_url)
- else -> getString(R.string.maps_streets_tile_url)
+ R.id.layerStreets -> R.array.maps_streets_tile
+ R.id.layerSatellite -> R.array.maps_satellite_tile
+ else -> R.array.maps_streets_tile
}
)
+ unitsViewModel.setMapLayer(MapLayer(
+ layer[0], layer[1].toInt(), layer[2].toInt(), layer[3]
+ ))
true
}
popOver.show()
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt
index 08d35a2..d3060f0 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt
@@ -2,12 +2,11 @@ package mx.trackermap.TrackerMap.android.units
import android.util.Log
import androidx.lifecycle.*
-import com.mousebird.maply.RemoteTileInfoNew
-import com.mousebird.maply.TileInfoNew
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import mx.trackermap.TrackerMap.client.models.Geofence
+import mx.trackermap.TrackerMap.client.models.MapLayer
import mx.trackermap.TrackerMap.client.models.UnitInformation
import mx.trackermap.TrackerMap.controllers.GeofencesController
import mx.trackermap.TrackerMap.controllers.UnitsController
@@ -30,14 +29,14 @@ class UnitsViewModel(
private var _unitsDisplayMode = MutableLiveData(UnitsDisplayMode.MAP)
private var _units = MutableLiveData<List<UnitInformation>>()
private var _selectedUnit = MutableLiveData<UnitInformation?>()
- private var _mapTileInfo = MutableLiveData<TileInfoNew>()
+ private var _mapLayer = MutableLiveData<MapLayer>()
private var _geofences = MutableLiveData<Map<Int, Geofence>>()
val searchQuery: LiveData<String> get() = _searchQuery
val unitsDisplayMode: LiveData<UnitsDisplayMode> get() = _unitsDisplayMode
val units: LiveData<List<UnitInformation>> get() = _units
val selectedUnit: LiveData<UnitInformation?> get() = _selectedUnit
- val mapTileInfo: LiveData<TileInfoNew> get() = _mapTileInfo
+ val mapLayer: LiveData<MapLayer> get() = _mapLayer
val geofences: LiveData<Map<Int, Geofence>> get() = _geofences
init {
@@ -86,9 +85,8 @@ class UnitsViewModel(
_unitsDisplayMode.postValue(displayMode)
}
- fun setMapLayer(url: String, minZoom: Int = 0, maxZoom: Int = 21) {
- val tileInfo = RemoteTileInfoNew(url, minZoom, maxZoom)
- _mapTileInfo.postValue(tileInfo)
+ fun setMapLayer(layer: MapLayer) {
+ _mapLayer.postValue(layer)
}
fun toggleDisplayMode() {
diff --git a/androidApp/src/main/res/layout/unit_map_fragment.xml b/androidApp/src/main/res/layout/unit_map_fragment.xml
index 486f1ce..8395013 100644
--- a/androidApp/src/main/res/layout/unit_map_fragment.xml
+++ b/androidApp/src/main/res/layout/unit_map_fragment.xml
@@ -14,7 +14,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- android:name="mx.trackermap.TrackerMap.android.map.MapFragment"/>
+ android:name="mx.trackermap.TrackerMap.android.map.MapFragment"
+ tools:visibility="invisible"/>
<androidx.cardview.widget.CardView
android:id="@+id/mapUnitCard"
@@ -121,4 +122,24 @@
</androidx.cardview.widget.CardView>
+ <!-- Attribution -->
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="@color/colorAttributionBackground"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <TextView
+ android:id="@+id/attributionText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:text="Copyright (C) OpenStreetMap"
+ android:textSize="@dimen/attribution_text_size"
+ android:textColor="@color/colorAttributionText"
+ android:layout_margin="6dp" />
+
+ </FrameLayout>
+
</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/androidApp/src/main/res/values-es-rMX/map_layers.xml b/androidApp/src/main/res/values-es-rMX/map_layers.xml
new file mode 100644
index 0000000..eff37d9
--- /dev/null
+++ b/androidApp/src/main/res/values-es-rMX/map_layers.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="layer_streets">Calles</string>
+ <string name="layer_satellite">Satélite</string>
+</resources> \ No newline at end of file
diff --git a/androidApp/src/main/res/values-es-rMX/strings.xml b/androidApp/src/main/res/values-es-rMX/strings.xml
index 47e4716..0806870 100644
--- a/androidApp/src/main/res/values-es-rMX/strings.xml
+++ b/androidApp/src/main/res/values-es-rMX/strings.xml
@@ -19,9 +19,6 @@
<string name="switch_layer">Cambiar capa del mapa</string>
<string name="open_profile">Ver información de cuenta</string>
- <string name="layer_streets">Calles</string>
- <string name="layer_satellite">Satélite</string>
-
<string name="menu_account">Cuenta</string>
<string name="menu_about">Acerca de</string>
<string name="menu_logout">Cerrar sesión</string>
diff --git a/androidApp/src/main/res/values/colors.xml b/androidApp/src/main/res/values/colors.xml
index bb4ab14..23fe6fb 100644
--- a/androidApp/src/main/res/values/colors.xml
+++ b/androidApp/src/main/res/values/colors.xml
@@ -12,6 +12,8 @@
<color name="colorGeofenceLabel">#1976D2</color>
<color name="colorGeofenceLabelOutline">#FFFFFF</color>
<color name="colorReport">#388E3C</color>
+ <color name="colorAttributionText">#66000000</color>
+ <color name="colorAttributionBackground">#4DFFFFFF</color>
<color name="colorOnline">#388E3C</color>
<color name="colorOffline">#D32F2F</color>
diff --git a/androidApp/src/main/res/values/dimen.xml b/androidApp/src/main/res/values/dimen.xml
index 271a12b..92d5242 100644
--- a/androidApp/src/main/res/values/dimen.xml
+++ b/androidApp/src/main/res/values/dimen.xml
@@ -33,6 +33,7 @@
<dimen name="geofence_label_text_size">11sp</dimen>
<dimen name="geofence_label_width">4dp</dimen>
<dimen name="report_label_width">10dp</dimen>
+ <dimen name="attribution_text_size">11sp</dimen>
<!-- User Information -->
<dimen name="fields_spacing">8dp</dimen>
diff --git a/androidApp/src/main/res/values/map_layers.xml b/androidApp/src/main/res/values/map_layers.xml
new file mode 100644
index 0000000..bffceb9
--- /dev/null
+++ b/androidApp/src/main/res/values/map_layers.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="layer_streets">Streets</string>
+ <string name="layer_satellite">Satellite</string>
+
+ <!-- Tile URLs -->
+ <!-- [0] = tile server URL -->
+ <!-- [1] = min zoom -->
+ <!-- [2] = max zoom -->
+ <!-- [3] = attribution text -->
+
+ <string-array name="maps_streets_tile" translatable="false">
+ <item>https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png</item>
+ <item>0</item>
+ <item>21</item>
+ <item>&#169; OpenStreetMap France | &#169; &lt;a href="https://www.openstreetmap.org/copyright"&gt;OpenStreetMap&lt;/a&gt; contributors</item>
+ </string-array>
+
+ <string-array name="maps_satellite_tile" translatable="false">
+ <item>https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}</item>
+ <item>0</item>
+ <item>20</item>
+ <item>Tiles &#169; Esri &#8212; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml
index 1caf502..1cc1f10 100644
--- a/androidApp/src/main/res/values/strings.xml
+++ b/androidApp/src/main/res/values/strings.xml
@@ -10,13 +10,6 @@
https://www.google.com/maps/place/%1$f,%2$f?z=19
</string>
- <string name="maps_streets_tile_url" translatable="false">
- https://mt0.google.com/vt/lyrs=m&amp;hl=en&amp;x={x}&amp;y={y}&amp;z={z}&amp;s=Ga
- </string>
- <string name="maps_satellite_url" translatable="false">
- https://mt0.google.com/vt/lyrs=y&amp;hl=en&amp;x={x}&amp;y={y}&amp;z={z}&amp;s=Ga
- </string>
-
<string name="notification_channel_id" translatable="false">default</string>
<string name="notification_channel" translatable="false">Default</string>
@@ -39,10 +32,6 @@
<string name="switch_layer">Switch map layer</string>
<string name="open_profile">View account info</string>
- <string name="layer_streets">Streets</string>
- <string name="layer_satellite">Satellite</string>
- <string name="layer_hybrid">Hybrid</string>
-
<string name="menu_account">Account</string>
<string name="menu_about">About</string>
<string name="menu_logout">Logout</string>
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/MapLayer.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/MapLayer.kt
new file mode 100644
index 0000000..64381d9
--- /dev/null
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/MapLayer.kt
@@ -0,0 +1,8 @@
+package mx.trackermap.TrackerMap.client.models
+
+data class MapLayer(
+ val url: String,
+ val minZoom: Int,
+ val maxZoom: Int,
+ val attribution: String
+) \ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/MapCalculus.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/MapCalculus.kt
new file mode 100644
index 0000000..118c117
--- /dev/null
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/MapCalculus.kt
@@ -0,0 +1,37 @@
+package mx.trackermap.TrackerMap.utils
+
+class MapCalculus {
+ companion object {
+ /**
+ * WhirlyGlobe library uses height rather than zoom levels, but it supports converting
+ * Mapnik denominator scales to height, so we can first convert zoom levels to Mapnik
+ * denominator scales, and then convert them to height using WhirlyGlobe.
+ * Source: https://github.com/openstreetmap/mapnik-stylesheets/blob/master/zoom-to-scale.txt
+ */
+ fun zoomLevelToScale(zoom: Int): Double? =
+ when (zoom) {
+ 1 -> 279541132.014
+ 2 -> 139770566.007
+ 3 -> 69885283.0036
+ 4 -> 34942641.5018
+ 5 -> 17471320.7509
+ 6 -> 8735660.37545
+ 7 -> 4367830.18772
+ 8 -> 2183915.09386
+ 9 -> 1091957.54693
+ 10 -> 545978.773466
+ 11 -> 272989.386733
+ 12 -> 136494.693366
+ 13 -> 68247.3466832
+ 14 -> 34123.6733416
+ 15 -> 17061.8366708
+ 16 -> 8530.9183354
+ 17 -> 4265.4591677
+ 18 -> 2132.72958385
+ 19 -> 1066.36479193
+ 20 -> 533.182395965
+ 21 -> 266.5911979825
+ else -> null
+ }
+ }
+} \ No newline at end of file