Meshtastic-Android/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt
2022-08-23 20:27:14 -04:00

190 lines
6.4 KiB
Kotlin

package com.geeksville.mesh.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
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.model.UIViewModel
import com.geeksville.util.formatAgo
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.OverlayItem
@AndroidEntryPoint
class MapFragment : ScreenFragment("Map"), Logging {
private lateinit var map: MapView
private lateinit var mapController: IMapController
private lateinit var nodePositions: List<GeoPoint>
private val model: UIViewModel by activityViewModels()
private val defaultLat = 38.8976763
private val defaultLong = -77.0365298
private val defaultZoomLevel = 6.0
private val defaultZoomSpeed = 3000L
private val defaultMinZoom = 3.0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.map_view, container, false)
}
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
val locations = nodesWithPosition.map { node ->
val p = node.position!!
debug("Showing on map: $node")
val f = GeoPoint(p.latitude, p.longitude)
node.user?.let {
OverlayItem("name", it.longName + " " + formatAgo(p.time), f)
}
f
}
nodePositions = locations
}
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) as MapView
/**
* Copyright layer required
*/
////////////////////////////////////////////////////////////
val copyrightNotice: String =
map.tileProvider.tileSource.copyrightNotice
val copyrightOverlay = CopyrightOverlay(context)
copyrightOverlay.setCopyrightNotice(copyrightNotice)
map.overlays.add(copyrightOverlay)
///////////////////////////////////////////////////////////
setupMapProperties()
if (view != null) {
val markerIcon =
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_twotone_person_pin_24
)!!.toBitmap()
// Provide initial positions
model.nodeDB.nodes.value?.let { nodes ->
onNodesChanged(nodes.values)
}
zoomToNodes(mapController)
// Any times nodes change update our map
model.nodeDB.nodes.observe(viewLifecycleOwner) { nodes ->
onNodesChanged(nodes.values)
}
}
}
private fun setupMapProperties() {
if (this::map.isInitialized) {
map.setTileSource(loadOnlineTileSourceBase())
map.minZoomLevel = defaultMinZoom
map.setMultiTouchControls(true) // Sets gesture controls to true
map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) // Disables default +/- button for zooming
mapController = map.controller
val point = GeoPoint(defaultLat, defaultLong) //White House Coordinates, Washington DC
mapController.animateTo(point, defaultZoomLevel, defaultZoomSpeed)
}
}
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()) {
val unit = if (nodesWithPosition.size >= 2) {
// Multiple nodes, make them all fit on the map view
nodesWithPosition.forEach {
points.add(
GeoPoint(
it.position!!.longitude,
it.position!!.latitude
)
)
}
map.zoomToBoundingBox(BoundingBox.fromGeoPoints(points), true)
} else {
// Only one node, just zoom in on it
val it = nodesWithPosition[0].position!!
points.add(GeoPoint(it.longitude, it.latitude))
controller.animateTo(points[0], defaultZoomLevel, defaultZoomSpeed)
}
}
}
private fun loadOnlineTileSourceBase(): OnlineTileSourceBase {
val prefs = context?.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
val mapSourceId = prefs?.getInt("map_style_id", 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
5 -> TileSourceFactory.CLOUDMADESMALLTILES
6 -> TileSourceFactory.ChartbundleENRH
7 -> TileSourceFactory.ChartbundleWAC
else -> TileSourceFactory.MAPNIK
}
return mapSource
}
override fun onPause() {
map.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
map.onResume()
}
override fun onDestroy() {
super.onDestroy()
map.onDetach()
}
}