Meshtastic-Android/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt
2022-08-24 23:15:48 -04:00

255 lines
9.3 KiB
Kotlin

package com.geeksville.mesh.ui
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import com.geeksville.android.Logging
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.MapViewBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.util.formatAgo
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
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.CopyrightOverlay
import org.osmdroid.views.overlay.Marker
@AndroidEntryPoint
class MapFragment : ScreenFragment("Map"), Logging {
private lateinit var binding: MapViewBinding
private lateinit var map: MapView
private lateinit var mapController: IMapController
private lateinit var mPrefs: SharedPreferences
private val model: UIViewModel by activityViewModels()
private val defaultMinZoom = 3.0
private val nodeZoomLevel = 8.5
private val defaultZoomSpeed = 3000L
private val prefsName = "org.andnav.osm.prefs"
private val prefsZoomLevelDouble = "prefsZoomLevelDouble"
private val prefsTileSource = "prefsTileSource"
private val mapStyleId = "map_style_id"
private val mapTag = "mapView"
private val uiPrefs = "ui-prefs"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = MapViewBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(viewIn: View, savedInstanceState: Bundle?) {
super.onViewCreated(viewIn, savedInstanceState)
Configuration.getInstance().userAgentValue =
BuildConfig.APPLICATION_ID // Required to get online tiles
map = viewIn.findViewById(R.id.map)
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)
}
}
}
// Any times nodes change update our map
model.nodeDB.nodes.observe(viewLifecycleOwner) { nodes ->
onNodesChanged(nodes.values)
}
zoomToNodes(mapController)
}
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")
builder.setSingleChoiceItems(mapStyles, mapStyleInt) { dialog, which ->
debug("Set mapStyleId pref to $which")
editor.putInt(mapStyleId, which)
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
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
)
map.overlays.add(marker)
}
f
}
}
/**
* 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)
}
private fun setupMapProperties() {
if (this::map.isInitialized) {
map.setDestroyMode(false)
map.isTilesScaledToDpi =
true // scales the map tiles to the display density of the screen
map.minZoomLevel =
defaultMinZoom // sets the minimum zoom level (the furthest out you can zoom)
map.setMultiTouchControls(true) // Sets gesture controls to true
map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) // Disables default +/- button for zooming
}
}
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
nodesWithPosition.forEach {
points.add(
GeoPoint(
it.position!!.latitude,
it.position!!.longitude
)
)
}
map.zoomToBoundingBox(
BoundingBox.fromGeoPoints(points),
true,
15,
nodeZoomLevel,
defaultZoomSpeed
)
} 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)
}
}
}
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
3 -> TileSourceFactory.OpenTopo
4 -> TileSourceFactory.ROADS_OVERLAY_NL
6 -> TileSourceFactory.ChartbundleENRH
7 -> TileSourceFactory.ChartbundleWAC
else -> TileSourceFactory.OpenTopo
}
return mapSource
}
override fun onPause() {
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()
val tileSourceName = mPrefs.getString(
prefsTileSource,
TileSourceFactory.DEFAULT_TILE_SOURCE.name()
)
try {
map.setTileSource(matchOnlineTileSourceBase(tileSourceName!!))
} catch (e: IllegalArgumentException) {
map.setTileSource(TileSourceFactory.DEFAULT_TILE_SOURCE)
}
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
TileSourceFactory.ROADS_OVERLAY_NL.name() -> TileSourceFactory.ROADS_OVERLAY_NL
TileSourceFactory.ChartbundleENRH.name() -> TileSourceFactory.ChartbundleENRH
TileSourceFactory.ChartbundleWAC.name() -> TileSourceFactory.ChartbundleWAC
else -> TileSourceFactory.DEFAULT_TILE_SOURCE
}
return tileSourceBase
}
override fun onDestroy() {
super.onDestroyView()
map.onDetach()
}
}