Meshtastic-Android/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt

279 lines
10 KiB
Kotlin
Raw Normal View History

2020-03-11 14:45:49 -07:00
package com.geeksville.mesh.ui
import android.content.Context
2022-08-24 11:08:39 -04:00
import android.content.SharedPreferences
2020-03-30 11:56:59 -07:00
import android.os.Bundle
2020-04-07 11:27:51 -07:00
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
2021-02-21 11:34:43 +08:00
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
2020-03-30 10:26:16 -07:00
import com.geeksville.android.Logging
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.NodeInfo
2020-03-11 18:13:44 -07:00
import com.geeksville.mesh.R
2022-08-24 12:16:57 -04:00
import com.geeksville.mesh.databinding.MapViewBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.util.formatAgo
2022-08-24 12:16:57 -04:00
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.osmdroid.api.IMapController
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
2022-08-25 10:14:19 -04:00
import org.osmdroid.tileprovider.tilesource.TileSourcePolicy
2022-08-23 20:27:14 -04:00
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
2022-08-25 10:14:19 -04:00
import org.osmdroid.util.MapTileIndex
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.CopyrightOverlay
import org.osmdroid.views.overlay.Marker
2020-03-30 12:47:01 -07:00
@AndroidEntryPoint
2020-04-07 11:27:51 -07:00
class MapFragment : ScreenFragment("Map"), Logging {
2020-04-07 12:13:50 -07:00
2022-08-24 12:16:57 -04:00
private lateinit var binding: MapViewBinding
private lateinit var map: MapView
private lateinit var mapController: IMapController
2022-08-24 11:08:39 -04:00
private lateinit var mPrefs: SharedPreferences
private val model: UIViewModel by activityViewModels()
2022-08-25 10:14:19 -04:00
private lateinit var esriTileSource: OnlineTileSourceBase
2022-08-25 10:14:19 -04:00
private val defaultMinZoom = 3.0
private val nodeZoomLevel = 8.5
private val defaultZoomSpeed = 3000L
2022-08-24 11:08:39 -04:00
private val prefsName = "org.andnav.osm.prefs"
private val prefsZoomLevelDouble = "prefsZoomLevelDouble"
private val prefsTileSource = "prefsTileSource"
private val mapStyleId = "map_style_id"
private val uiPrefs = "ui-prefs"
2020-04-07 12:13:50 -07:00
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
2022-08-24 23:15:48 -04:00
binding = MapViewBinding.inflate(inflater)
2022-08-24 12:16:57 -04:00
return binding.root
2022-08-24 11:08:39 -04:00
}
override fun onViewCreated(viewIn: View, savedInstanceState: Bundle?) {
super.onViewCreated(viewIn, savedInstanceState)
Configuration.getInstance().userAgentValue =
BuildConfig.APPLICATION_ID // Required to get online tiles
2022-08-24 23:15:48 -04:00
map = viewIn.findViewById(R.id.map)
2022-08-24 11:08:39 -04:00
mPrefs = context!!.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
addCopyright() // Copyright is required for certain map sources
setupMapProperties()
loadOnlineTileSourceBase()
mapController = map.controller
map.let {
if (view != null) {
binding.fabStyleToggle.setOnClickListener {
chooseMapStyle()
}
model.nodeDB.nodes.value?.let { nodes ->
onNodesChanged(nodes.values)
}
2022-08-24 11:08:39 -04:00
}
}
2022-08-24 11:08:39 -04:00
// Any times nodes change update our map
model.nodeDB.nodes.observe(viewLifecycleOwner) { nodes ->
onNodesChanged(nodes.values)
2022-08-24 11:08:39 -04:00
}
zoomToNodes(mapController)
}
2022-08-24 12:16:57 -04:00
private fun chooseMapStyle() {
/// Prepare dialog and its items
val builder = MaterialAlertDialogBuilder(context!!)
builder.setTitle(getString(R.string.preferences_map_style))
val mapStyles by lazy { resources.getStringArray(R.array.map_styles) }
/// Load preferences and its value
val prefs = UIViewModel.getPreferences(context!!)
val editor: SharedPreferences.Editor = prefs.edit()
val mapStyleInt = prefs.getInt(mapStyleId, 1)
debug("mapStyleId from prefs: $mapStyleInt")
2022-08-24 12:16:57 -04:00
builder.setSingleChoiceItems(mapStyles, mapStyleInt) { dialog, which ->
2022-08-24 12:16:57 -04:00
debug("Set mapStyleId pref to $which")
editor.putInt(mapStyleId, which)
2022-08-24 12:16:57 -04:00
editor.apply()
dialog.dismiss()
}
val dialog = builder.create()
dialog.show()
}
private fun onNodesChanged(nodes: Collection<NodeInfo>) {
val nodesWithPosition = nodes.filter { it.validPosition != null }
/**
* Using the latest nodedb, generate GeoPoint
*/
// Find all nodes with valid locations
2020-04-07 12:13:50 -07:00
nodesWithPosition.map { node ->
val p = node.position!!
debug("Showing on map: $node")
val f = GeoPoint(p.latitude, p.longitude)
node.user?.let {
val marker = Marker(map)
marker.title = it.longName + " " + formatAgo(p.time)
marker.setAnchor(Marker.ANCHOR_BOTTOM, Marker.ANCHOR_CENTER)
marker.position = f
marker.icon = ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_twotone_person_pin_24
)
2022-08-24 11:08:39 -04:00
map.overlays.add(marker)
}
f
}
}
2020-04-07 12:13:50 -07:00
2022-08-24 11:08:39 -04:00
/**
* Adds copyright to map depending on what source is showing
*/
private fun addCopyright() {
val copyrightNotice: String =
map.tileProvider.tileSource.copyrightNotice
val copyrightOverlay = CopyrightOverlay(context)
copyrightOverlay.setCopyrightNotice(copyrightNotice)
map.overlays.add(copyrightOverlay)
2021-02-06 22:08:49 -08:00
}
private fun setupMapProperties() {
if (this::map.isInitialized) {
2022-08-25 10:14:19 -04:00
map.setDestroyMode(false) // keeps map instance alive when in the background.
2022-08-24 11:08:39 -04:00
map.isTilesScaledToDpi =
true // scales the map tiles to the display density of the screen
2022-08-24 11:08:39 -04:00
map.minZoomLevel =
defaultMinZoom // sets the minimum zoom level (the furthest out you can zoom)
2022-08-25 10:14:19 -04:00
map.setMultiTouchControls(true) // Sets gesture controls to true.
map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) // Disables default +/- button for zooming
2022-08-25 10:14:19 -04:00
setESRITileSource()
}
}
private fun zoomToNodes(controller: IMapController) {
val points: MutableList<GeoPoint> = mutableListOf()
val nodesWithPosition =
model.nodeDB.nodes.value?.values?.filter { it.validPosition != null }
if ((nodesWithPosition != null) && nodesWithPosition.isNotEmpty()) {
if (nodesWithPosition.size >= 2) {
// Multiple nodes, make them all fit on the map view
2022-02-05 22:03:51 -05:00
nodesWithPosition.forEach {
points.add(
GeoPoint(
it.position!!.latitude,
it.position!!.longitude
2022-02-05 22:03:51 -05:00
)
)
}
map.zoomToBoundingBox(
BoundingBox.fromGeoPoints(points),
true,
15,
nodeZoomLevel,
defaultZoomSpeed
)
2022-02-05 20:21:42 -05:00
} else {
// Only one node, just zoom in on it
val it = nodesWithPosition[0].position!!
points.add(GeoPoint(it.latitude, it.longitude))
controller.animateTo(points[0], nodeZoomLevel, defaultZoomSpeed)
2022-02-17 10:16:58 -05:00
}
}
}
2022-08-25 10:14:19 -04:00
private fun setESRITileSource() {
esriTileSource = object : OnlineTileSourceBase(
"ESRI Clarity", 0, 18, 256, "", arrayOf(
"https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/WMTS/1.0.0/default028mm/MapServer/tile/"
), "Esri, Maxar, Earthstar Geographics, and the GIS User Community" +
"URL\n" +
"View\n",
TileSourcePolicy(
2, TileSourcePolicy.FLAG_NO_BULK
or TileSourcePolicy.FLAG_NO_PREVENTIVE
or TileSourcePolicy.FLAG_USER_AGENT_MEANINGFUL
or TileSourcePolicy.FLAG_USER_AGENT_NORMALIZED
)
) {
override fun getTileURLString(pMapTileIndex: Long): String {
return baseUrl + (MapTileIndex.getZoom(pMapTileIndex)
.toString() + "/" + MapTileIndex.getY(pMapTileIndex)
+ "/" + MapTileIndex.getX(pMapTileIndex)
+ mImageFilenameEnding)
}
}
}
private fun loadOnlineTileSourceBase(): OnlineTileSourceBase {
val prefs = context?.getSharedPreferences(uiPrefs, Context.MODE_PRIVATE)
val mapSourceId = prefs?.getInt(mapStyleId, 1)
debug("mapStyleId from prefs: $mapSourceId")
val mapSource = when (mapSourceId) {
0 -> TileSourceFactory.MAPNIK
1 -> TileSourceFactory.USGS_TOPO
2 -> TileSourceFactory.USGS_SAT
2022-08-25 10:14:19 -04:00
3 -> esriTileSource
else -> TileSourceFactory.DEFAULT_TILE_SOURCE
2022-03-06 19:52:49 -05:00
}
return mapSource
}
override fun onPause() {
2022-08-24 11:08:39 -04:00
val edit = mPrefs.edit()
edit.putString(prefsTileSource, loadOnlineTileSourceBase().name())
edit.putFloat(prefsZoomLevelDouble, map.zoomLevelDouble.toFloat())
edit.commit()
map.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
2022-08-25 10:14:19 -04:00
map.invalidate()
2022-08-24 11:08:39 -04:00
val tileSourceName = mPrefs.getString(
prefsTileSource,
TileSourceFactory.DEFAULT_TILE_SOURCE.name()
)
try {
2022-08-24 23:15:48 -04:00
map.setTileSource(matchOnlineTileSourceBase(tileSourceName!!))
2022-08-24 11:08:39 -04:00
} catch (e: IllegalArgumentException) {
map.setTileSource(TileSourceFactory.DEFAULT_TILE_SOURCE)
}
2022-08-24 23:15:48 -04:00
map.onResume()
}
private fun matchOnlineTileSourceBase(name: String): OnlineTileSourceBase {
val tileSourceBase = when (name) {
TileSourceFactory.MAPNIK.name() -> TileSourceFactory.MAPNIK
TileSourceFactory.USGS_TOPO.name() -> TileSourceFactory.USGS_TOPO
TileSourceFactory.USGS_SAT.name() -> TileSourceFactory.USGS_SAT
2022-08-25 10:14:19 -04:00
esriTileSource.name() -> esriTileSource
2022-08-24 23:15:48 -04:00
else -> TileSourceFactory.DEFAULT_TILE_SOURCE
}
return tileSourceBase
2022-02-17 15:22:22 -05:00
}
override fun onDestroy() {
2022-08-24 11:08:39 -04:00
super.onDestroyView()
map.onDetach()
2022-02-17 15:22:22 -05:00
}
2020-03-11 14:45:49 -07:00
}
2020-04-07 11:27:51 -07:00