2020-03-11 14:45:49 -07:00
|
|
|
package com.geeksville.mesh.ui
|
|
|
|
|
|
2022-02-16 10:22:59 -05:00
|
|
|
import android.app.AlertDialog
|
|
|
|
|
import android.content.Intent
|
2020-03-30 15:00:18 -07:00
|
|
|
import android.graphics.Color
|
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
|
2022-02-05 13:26:08 -05:00
|
|
|
import androidx.core.graphics.drawable.toBitmap
|
2022-02-04 21:58:00 -05:00
|
|
|
import androidx.core.view.isVisible
|
2020-04-08 09:53:04 -07:00
|
|
|
import androidx.fragment.app.activityViewModels
|
2020-07-07 14:55:01 -07:00
|
|
|
import androidx.lifecycle.Observer
|
2020-04-11 13:20:30 -07:00
|
|
|
import com.geeksville.android.GeeksvilleApplication
|
2020-03-30 10:26:16 -07:00
|
|
|
import com.geeksville.android.Logging
|
2020-04-08 09:53:04 -07:00
|
|
|
import com.geeksville.mesh.NodeInfo
|
2020-03-11 18:13:44 -07:00
|
|
|
import com.geeksville.mesh.R
|
2020-04-08 09:53:04 -07:00
|
|
|
import com.geeksville.mesh.model.UIViewModel
|
2021-03-16 00:05:28 +05:00
|
|
|
import com.geeksville.util.formatAgo
|
2020-03-30 12:47:01 -07:00
|
|
|
import com.mapbox.geojson.Feature
|
|
|
|
|
import com.mapbox.geojson.FeatureCollection
|
|
|
|
|
import com.mapbox.geojson.Point
|
2022-02-05 20:21:42 -05:00
|
|
|
import com.mapbox.maps.*
|
2022-02-05 22:01:46 -05:00
|
|
|
import com.mapbox.maps.dsl.cameraOptions
|
2022-02-04 21:58:00 -05:00
|
|
|
import com.mapbox.maps.extension.style.expressions.generated.Expression
|
2022-02-05 13:26:08 -05:00
|
|
|
import com.mapbox.maps.extension.style.layers.addLayer
|
2022-02-04 21:58:00 -05:00
|
|
|
import com.mapbox.maps.extension.style.layers.generated.SymbolLayer
|
|
|
|
|
import com.mapbox.maps.extension.style.layers.properties.generated.IconAnchor
|
|
|
|
|
import com.mapbox.maps.extension.style.layers.properties.generated.TextAnchor
|
|
|
|
|
import com.mapbox.maps.extension.style.layers.properties.generated.TextJustify
|
2022-02-05 13:26:08 -05:00
|
|
|
import com.mapbox.maps.extension.style.sources.addSource
|
2022-02-04 21:58:00 -05:00
|
|
|
import com.mapbox.maps.extension.style.sources.generated.GeoJsonSource
|
2022-02-05 20:21:42 -05:00
|
|
|
import com.mapbox.maps.plugin.animation.MapAnimationOptions
|
2022-02-05 22:01:46 -05:00
|
|
|
import com.mapbox.maps.plugin.animation.flyTo
|
2022-02-16 10:22:59 -05:00
|
|
|
import com.mapbox.maps.plugin.gestures.OnMapLongClickListener
|
2022-02-05 16:44:39 -05:00
|
|
|
import com.mapbox.maps.plugin.gestures.gestures
|
2022-02-08 13:50:21 -08:00
|
|
|
import dagger.hilt.android.AndroidEntryPoint
|
2020-03-30 12:47:01 -07:00
|
|
|
|
2020-03-11 14:45:49 -07:00
|
|
|
|
2022-02-08 13:50:21 -08:00
|
|
|
@AndroidEntryPoint
|
2020-04-07 11:27:51 -07:00
|
|
|
class MapFragment : ScreenFragment("Map"), Logging {
|
2020-04-07 12:13:50 -07:00
|
|
|
|
2020-04-08 09:53:04 -07:00
|
|
|
private val model: UIViewModel by activityViewModels()
|
|
|
|
|
|
2020-04-07 12:13:50 -07:00
|
|
|
private val nodeSourceId = "node-positions"
|
|
|
|
|
private val nodeLayerId = "node-layer"
|
|
|
|
|
private val labelLayerId = "label-layer"
|
|
|
|
|
private val markerImageId = "my-marker-image"
|
|
|
|
|
|
2022-02-16 10:22:59 -05:00
|
|
|
|
|
|
|
|
private val userTouchPositionId = "user-touch-position"
|
|
|
|
|
private val userTouchLayerId = "user-touch-layer"
|
2022-02-05 20:21:42 -05:00
|
|
|
private var nodePositions = GeoJsonSource(GeoJsonSource.Builder(nodeSourceId))
|
2020-04-07 12:13:50 -07:00
|
|
|
|
2022-02-16 13:56:17 -05:00
|
|
|
private val userTouchPosition = GeoJsonSource(GeoJsonSource.Builder(userTouchPositionId))
|
|
|
|
|
|
|
|
|
|
|
2022-02-04 21:58:00 -05:00
|
|
|
private val nodeLayer = SymbolLayer(nodeLayerId, nodeSourceId)
|
|
|
|
|
.iconImage(markerImageId)
|
|
|
|
|
.iconAnchor(IconAnchor.BOTTOM)
|
2022-02-05 12:32:31 -05:00
|
|
|
.iconAllowOverlap(true)
|
2020-04-07 12:13:50 -07:00
|
|
|
|
2022-02-16 10:22:59 -05:00
|
|
|
private val userTouchLayer = SymbolLayer(userTouchLayerId, userTouchPositionId)
|
|
|
|
|
.iconImage(markerImageId)
|
|
|
|
|
.iconAnchor(IconAnchor.BOTTOM)
|
|
|
|
|
.iconAllowOverlap(true)
|
|
|
|
|
|
2022-02-04 21:58:00 -05:00
|
|
|
private val labelLayer = SymbolLayer(labelLayerId, nodeSourceId)
|
|
|
|
|
.textField(Expression.get("name"))
|
2022-02-05 13:26:08 -05:00
|
|
|
.textSize(12.0)
|
2022-02-04 21:58:00 -05:00
|
|
|
.textColor(Color.RED)
|
2022-02-05 13:26:08 -05:00
|
|
|
.textAnchor(TextAnchor.TOP)
|
|
|
|
|
//.textVariableAnchor(TextAnchor.TOP) //TODO investigate need for variable anchor vs normal anchor
|
2022-02-04 21:58:00 -05:00
|
|
|
.textJustify(TextJustify.AUTO)
|
|
|
|
|
.textAllowOverlap(true)
|
2020-04-07 12:13:50 -07:00
|
|
|
|
|
|
|
|
|
2020-04-08 09:53:04 -07:00
|
|
|
private fun onNodesChanged(map: MapboxMap, nodes: Collection<NodeInfo>) {
|
|
|
|
|
val nodesWithPosition = nodes.filter { it.validPosition != null }
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Using the latest nodedb, generate geojson features
|
|
|
|
|
*/
|
|
|
|
|
fun getCurrentNodes(): FeatureCollection {
|
|
|
|
|
// Find all nodes with valid locations
|
2020-04-07 12:13:50 -07:00
|
|
|
|
2020-04-08 09:53:04 -07:00
|
|
|
val locations = nodesWithPosition.map { node ->
|
|
|
|
|
val p = node.position!!
|
|
|
|
|
debug("Showing on map: $node")
|
2020-04-07 12:13:50 -07:00
|
|
|
|
2020-04-08 09:53:04 -07:00
|
|
|
val f = Feature.fromGeometry(
|
|
|
|
|
Point.fromLngLat(
|
|
|
|
|
p.longitude,
|
|
|
|
|
p.latitude
|
|
|
|
|
)
|
2020-04-07 12:13:50 -07:00
|
|
|
)
|
2020-04-08 09:53:04 -07:00
|
|
|
node.user?.let {
|
2021-03-16 10:04:01 +05:00
|
|
|
f.addStringProperty("name", it.longName + " " + formatAgo(p.time))
|
2020-04-08 09:53:04 -07:00
|
|
|
}
|
|
|
|
|
f
|
2020-04-07 12:13:50 -07:00
|
|
|
}
|
|
|
|
|
|
2020-04-08 09:53:04 -07:00
|
|
|
return FeatureCollection.fromFeatures(locations)
|
|
|
|
|
}
|
2022-02-05 20:21:42 -05:00
|
|
|
nodePositions.featureCollection(getCurrentNodes())
|
2021-02-06 22:08:49 -08:00
|
|
|
}
|
|
|
|
|
|
2022-02-05 22:01:46 -05:00
|
|
|
private fun zoomToNodes(map: MapboxMap) {
|
|
|
|
|
val points: MutableList<Point> = mutableListOf()
|
2021-03-16 10:04:01 +05:00
|
|
|
val nodesWithPosition =
|
|
|
|
|
model.nodeDB.nodes.value?.values?.filter { it.validPosition != null }
|
2021-02-06 22:08:49 -08:00
|
|
|
if (nodesWithPosition != null && nodesWithPosition.isNotEmpty()) {
|
2022-02-05 22:01:46 -05:00
|
|
|
val unit = if (nodesWithPosition.size >= 2) {
|
2022-02-05 20:21:42 -05:00
|
|
|
|
2022-02-05 22:01:46 -05:00
|
|
|
// Multiple nodes, make them all fit on the map view
|
2022-02-05 22:03:51 -05:00
|
|
|
nodesWithPosition.forEach {
|
|
|
|
|
points.add(
|
|
|
|
|
Point.fromLngLat(
|
|
|
|
|
it.position!!.longitude,
|
|
|
|
|
it.position!!.latitude
|
|
|
|
|
)
|
|
|
|
|
)
|
2022-02-05 22:01:46 -05:00
|
|
|
}
|
|
|
|
|
map.cameraForCoordinates(points)
|
2022-02-05 20:21:42 -05:00
|
|
|
} else {
|
|
|
|
|
// Only one node, just zoom in on it
|
|
|
|
|
val it = nodesWithPosition[0].position!!
|
2022-02-05 22:01:46 -05:00
|
|
|
points.add(Point.fromLngLat(it.longitude, it.latitude))
|
|
|
|
|
map.cameraForCoordinates(points)
|
|
|
|
|
cameraOptions {
|
|
|
|
|
this.zoom(9.0)
|
|
|
|
|
this.center(points[0])
|
|
|
|
|
}
|
2022-02-05 20:21:42 -05:00
|
|
|
}
|
2022-02-05 22:01:46 -05:00
|
|
|
map.flyTo(
|
|
|
|
|
unit,
|
|
|
|
|
MapAnimationOptions.mapAnimationOptions { duration(1000) })
|
2021-02-06 22:08:49 -08:00
|
|
|
}
|
2020-04-07 12:13:50 -07:00
|
|
|
}
|
|
|
|
|
|
2020-04-07 11:27:51 -07:00
|
|
|
override fun onCreateView(
|
|
|
|
|
inflater: LayoutInflater, container: ViewGroup?,
|
|
|
|
|
savedInstanceState: Bundle?
|
2020-04-11 13:20:30 -07:00
|
|
|
): View? {
|
|
|
|
|
// We can't allow mapbox if user doesn't want analytics
|
|
|
|
|
val id =
|
|
|
|
|
if ((requireContext().applicationContext as GeeksvilleApplication).isAnalyticsAllowed) {
|
|
|
|
|
// Mapbox Access token
|
|
|
|
|
R.layout.map_view
|
|
|
|
|
} else {
|
|
|
|
|
R.layout.map_not_allowed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return inflater.inflate(id, container, false)
|
|
|
|
|
}
|
2020-03-11 14:45:49 -07:00
|
|
|
|
2020-04-11 13:20:30 -07:00
|
|
|
var mapView: MapView? = null
|
2020-07-07 14:55:01 -07:00
|
|
|
|
2021-02-08 12:19:35 +08:00
|
|
|
/**
|
|
|
|
|
* Mapbox native code can crash painfully if you ever call a mapbox view function while the view is not actively being show
|
|
|
|
|
*/
|
|
|
|
|
private val isViewVisible: Boolean
|
2022-02-04 21:58:00 -05:00
|
|
|
get() = mapView?.isVisible == true
|
2021-02-08 12:19:35 +08:00
|
|
|
|
2020-06-10 17:12:57 -07:00
|
|
|
override fun onViewCreated(viewIn: View, savedInstanceState: Bundle?) {
|
2020-06-10 17:19:17 -07:00
|
|
|
super.onViewCreated(viewIn, savedInstanceState)
|
2020-03-30 11:56:59 -07:00
|
|
|
|
2020-04-11 13:20:30 -07:00
|
|
|
// We might not have a real mapview if running with analytics
|
|
|
|
|
if ((requireContext().applicationContext as GeeksvilleApplication).isAnalyticsAllowed) {
|
2020-06-10 17:12:57 -07:00
|
|
|
val vIn = viewIn.findViewById<MapView>(R.id.mapView)
|
|
|
|
|
mapView = vIn
|
2020-06-10 12:49:05 -07:00
|
|
|
mapView?.let { v ->
|
2021-02-02 10:47:54 +08:00
|
|
|
|
2020-06-10 12:49:05 -07:00
|
|
|
// Each time the pane is shown start fetching new map info (we do this here instead of
|
|
|
|
|
// onCreate because getMapAsync can die in native code if the view goes away)
|
2020-07-07 14:55:01 -07:00
|
|
|
|
2022-02-04 21:58:00 -05:00
|
|
|
val map = v.getMapboxMap()
|
|
|
|
|
if (view != null) { // it might have gone away by now
|
|
|
|
|
val markerIcon =
|
|
|
|
|
ContextCompat.getDrawable(
|
|
|
|
|
requireActivity(),
|
|
|
|
|
R.drawable.ic_twotone_person_pin_24
|
2022-02-05 13:26:08 -05:00
|
|
|
)!!.toBitmap()
|
2022-02-04 21:58:00 -05:00
|
|
|
|
2022-02-05 13:26:08 -05:00
|
|
|
map.loadStyleUri(Style.OUTDOORS) {
|
2022-02-05 16:44:39 -05:00
|
|
|
if (it.isStyleLoaded) {
|
|
|
|
|
it.addSource(nodePositions)
|
|
|
|
|
it.addImage(markerImageId, markerIcon)
|
|
|
|
|
it.addLayer(nodeLayer)
|
|
|
|
|
it.addLayer(labelLayer)
|
|
|
|
|
}
|
2022-02-05 13:26:08 -05:00
|
|
|
}
|
2022-02-04 21:58:00 -05:00
|
|
|
|
2022-02-05 16:44:39 -05:00
|
|
|
v.gestures.rotateEnabled = false
|
2022-02-16 10:22:59 -05:00
|
|
|
v.gestures.addOnMapLongClickListener(this.longClick)
|
2022-02-05 12:32:31 -05:00
|
|
|
|
2022-02-04 21:58:00 -05:00
|
|
|
// Provide initial positions
|
|
|
|
|
model.nodeDB.nodes.value?.let { nodes ->
|
|
|
|
|
onNodesChanged(map, nodes.values)
|
|
|
|
|
}
|
2020-04-11 13:20:30 -07:00
|
|
|
}
|
2022-02-04 21:58:00 -05:00
|
|
|
|
|
|
|
|
// Any times nodes change update our map
|
|
|
|
|
model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { nodes ->
|
|
|
|
|
if (isViewVisible)
|
|
|
|
|
onNodesChanged(map, nodes.values)
|
|
|
|
|
})
|
|
|
|
|
zoomToNodes(map)
|
2020-04-11 13:20:30 -07:00
|
|
|
}
|
2020-03-30 10:26:16 -07:00
|
|
|
}
|
2020-03-11 14:45:49 -07:00
|
|
|
}
|
2022-02-16 10:22:59 -05:00
|
|
|
|
2022-02-16 13:56:17 -05:00
|
|
|
/**
|
|
|
|
|
* OnLongClick of the map set a position marker.
|
|
|
|
|
*/
|
2022-02-16 10:22:59 -05:00
|
|
|
private val longClick = OnMapLongClickListener {
|
|
|
|
|
val userDefinedPointImg =
|
|
|
|
|
ContextCompat.getDrawable(requireActivity(), R.drawable.ic_twotone_person_24)!!
|
|
|
|
|
.toBitmap()
|
|
|
|
|
val point = Point.fromLngLat(it.longitude(), it.latitude())
|
|
|
|
|
|
|
|
|
|
mapView?.getMapboxMap()?.getStyle()?.let { style ->
|
2022-02-16 13:56:17 -05:00
|
|
|
userTouchPosition.geometry(point)
|
|
|
|
|
|
|
|
|
|
if (!style.styleLayerExists(userTouchLayerId)) {
|
|
|
|
|
style.addImage("userImage", userDefinedPointImg)
|
|
|
|
|
style.addSource(userTouchPosition)
|
|
|
|
|
style.addLayer(userTouchLayer)
|
|
|
|
|
}
|
2022-02-16 10:22:59 -05:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return@OnMapLongClickListener true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class OfflineLog(val message: String, val color: Int) {
|
|
|
|
|
class Info(message: String) : OfflineLog(message, android.R.color.black)
|
|
|
|
|
class Error(message: String) : OfflineLog(message, android.R.color.holo_red_dark)
|
|
|
|
|
class Success(message: String) : OfflineLog(message, android.R.color.holo_green_dark)
|
|
|
|
|
class TilePackProgress(message: String) : OfflineLog(message, android.R.color.holo_purple)
|
|
|
|
|
class StylePackProgress(message: String) :
|
|
|
|
|
OfflineLog(message, android.R.color.holo_orange_dark)
|
|
|
|
|
}
|
2020-03-11 14:45:49 -07:00
|
|
|
}
|
2020-04-07 11:27:51 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|