kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: edit waypoints
rodzic
63ac168fc8
commit
ce66a9425d
|
@ -53,7 +53,7 @@ interface PacketDao {
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
fun getDataPacketById(requestId: Int): DataPacket? {
|
fun getDataPacketById(requestId: Int): DataPacket? {
|
||||||
return getDataPackets().firstOrNull { it.id == requestId }
|
return getDataPackets().lastOrNull { it.id == requestId }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
|
|
|
@ -161,17 +161,15 @@ class UIViewModel @Inject constructor(
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val contacts: LiveData<Map<String, Packet>> = _packets.mapLatest { list ->
|
val contacts: LiveData<Map<String, Packet>> = _packets.mapLatest { list ->
|
||||||
list.associateBy { packet -> packet.contact_key }
|
list.filter { it.port_num == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE }
|
||||||
.filter { it.value.port_num == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE }
|
.associateBy { packet -> packet.contact_key }
|
||||||
}.asLiveData()
|
}.asLiveData()
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val waypoints: LiveData<Map<Int?, Packet>> = _packets.mapLatest { list ->
|
val waypoints: LiveData<Map<Int, Packet>> = _packets.mapLatest { list ->
|
||||||
list.associateBy { packet -> packet.data.waypoint?.id }
|
list.filter { it.port_num == Portnums.PortNum.WAYPOINT_APP_VALUE }
|
||||||
.filterValues {
|
.associateBy { packet -> packet.data.waypoint!!.id }
|
||||||
val expired = (it.data.waypoint?.expire ?: 0) < System.currentTimeMillis() / 1000
|
.filterValues { it.data.waypoint!!.expire > System.currentTimeMillis() / 1000 }
|
||||||
it.port_num == Portnums.PortNum.WAYPOINT_APP_VALUE && !expired
|
|
||||||
}
|
|
||||||
}.asLiveData()
|
}.asLiveData()
|
||||||
|
|
||||||
fun generatePacketId(): Int? {
|
fun generatePacketId(): Int? {
|
||||||
|
@ -198,7 +196,7 @@ class UIViewModel @Inject constructor(
|
||||||
val dest = if (channel != null) contactKey.substring(1) else contactKey
|
val dest = if (channel != null) contactKey.substring(1) else contactKey
|
||||||
|
|
||||||
val p = DataPacket(dest, channel ?: 0, wpt)
|
val p = DataPacket(dest, channel ?: 0, wpt)
|
||||||
if (wpt.id != 0) sendDataPacket(p.copy(id = wpt.id))
|
if (wpt.id != 0) sendDataPacket(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendDataPacket(p: DataPacket) {
|
private fun sendDataPacket(p: DataPacket) {
|
||||||
|
@ -294,6 +292,7 @@ class UIViewModel @Inject constructor(
|
||||||
/// hardware info about our local device (can be null)
|
/// hardware info about our local device (can be null)
|
||||||
private val _myNodeInfo = MutableLiveData<MyNodeInfo?>()
|
private val _myNodeInfo = MutableLiveData<MyNodeInfo?>()
|
||||||
val myNodeInfo: LiveData<MyNodeInfo?> get() = _myNodeInfo
|
val myNodeInfo: LiveData<MyNodeInfo?> get() = _myNodeInfo
|
||||||
|
val myNodeNum get() = _myNodeInfo.value?.myNodeNum
|
||||||
|
|
||||||
fun setMyNodeInfo(info: MyNodeInfo?) {
|
fun setMyNodeInfo(info: MyNodeInfo?) {
|
||||||
_myNodeInfo.value = info
|
_myNodeInfo.value = info
|
||||||
|
@ -526,7 +525,7 @@ class UIViewModel @Inject constructor(
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
// Extract distances to this device from position messages and put (node,SNR,distance) in
|
// Extract distances to this device from position messages and put (node,SNR,distance) in
|
||||||
// the file_uri
|
// the file_uri
|
||||||
val myNodeNum = myNodeInfo.value?.myNodeNum ?: return@launch
|
val myNodeNum = myNodeNum ?: return@launch
|
||||||
|
|
||||||
// Capture the current node value while we're still on main thread
|
// Capture the current node value while we're still on main thread
|
||||||
val nodes = nodeDB.nodes.value ?: emptyMap()
|
val nodes = nodeDB.nodes.value ?: emptyMap()
|
||||||
|
|
|
@ -20,9 +20,11 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.geeksville.mesh.BuildConfig
|
import com.geeksville.mesh.BuildConfig
|
||||||
import com.geeksville.mesh.DataPacket
|
import com.geeksville.mesh.DataPacket
|
||||||
|
import com.geeksville.mesh.MeshProtos.Waypoint
|
||||||
import com.geeksville.mesh.NodeInfo
|
import com.geeksville.mesh.NodeInfo
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.android.Logging
|
import com.geeksville.mesh.android.Logging
|
||||||
|
import com.geeksville.mesh.copy
|
||||||
import com.geeksville.mesh.database.entity.Packet
|
import com.geeksville.mesh.database.entity.Packet
|
||||||
import com.geeksville.mesh.databinding.MapViewBinding
|
import com.geeksville.mesh.databinding.MapViewBinding
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
|
@ -57,6 +59,7 @@ import org.osmdroid.views.overlay.Polygon
|
||||||
import org.osmdroid.views.overlay.gridlines.LatLonGridlineOverlay2
|
import org.osmdroid.views.overlay.gridlines.LatLonGridlineOverlay2
|
||||||
import org.osmdroid.views.overlay.infowindow.InfoWindow
|
import org.osmdroid.views.overlay.infowindow.InfoWindow
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.text.DateFormat
|
||||||
import kotlin.math.log2
|
import kotlin.math.log2
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,7 +79,8 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging {
|
||||||
private val prefsName = "org.geeksville.osm.prefs"
|
private val prefsName = "org.geeksville.osm.prefs"
|
||||||
private val mapStyleId = "map_style_id"
|
private val mapStyleId = "map_style_id"
|
||||||
private var nodePositions = listOf<MarkerWithLabel>()
|
private var nodePositions = listOf<MarkerWithLabel>()
|
||||||
private var wayPoints = listOf<MarkerWithLabel>()
|
private var waypoints = mapOf<Int, Waypoint?>()
|
||||||
|
private var waypointMarkers = listOf<MarkerWithLabel>()
|
||||||
private val nodeLayer = 1
|
private val nodeLayer = 1
|
||||||
|
|
||||||
// Distance of bottom corner to top corner of bounding box
|
// Distance of bottom corner to top corner of bounding box
|
||||||
|
@ -133,6 +137,7 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging {
|
||||||
}
|
}
|
||||||
model.waypoints.observe(viewLifecycleOwner) {
|
model.waypoints.observe(viewLifecycleOwner) {
|
||||||
debug("New waypoints received: ${it.size}")
|
debug("New waypoints received: ${it.size}")
|
||||||
|
waypoints = it.mapValues { p -> p.value.data.waypoint }
|
||||||
onWaypointChanged(it.values)
|
onWaypointChanged(it.values)
|
||||||
drawOverlays()
|
drawOverlays()
|
||||||
}
|
}
|
||||||
|
@ -244,21 +249,88 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging {
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showMarkerLongPressDialog(id: Int) {
|
private data class DialogBuilder(
|
||||||
debug("marker long pressed id=${id}")
|
val builder: MaterialAlertDialogBuilder,
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
val nameInput: EditText,
|
||||||
.setTitle("${getString(R.string.delete)}?")
|
val descInput: EditText,
|
||||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
val lockedSwitch: SwitchMaterial,
|
||||||
debug("User canceled marker edit dialog")
|
) {
|
||||||
}
|
val name get() = nameInput.text.toString().trim()
|
||||||
// .setNegativeButton(R.string.edit) { _, _ ->
|
val description get() = descInput.text.toString().trim()
|
||||||
// debug("Negative button pressed") // TODO add Edit option
|
}
|
||||||
// }
|
|
||||||
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
private fun createEditDialog(context: Context, title: String): DialogBuilder {
|
||||||
debug("User deleted local waypoint $id")
|
val builder = MaterialAlertDialogBuilder(context)
|
||||||
|
val layout = LayoutInflater.from(context).inflate(R.layout.dialog_add_waypoint, null)
|
||||||
|
|
||||||
|
val nameInput: EditText = layout.findViewById(R.id.waypointName)
|
||||||
|
val descInput: EditText= layout.findViewById(R.id.waypointDescription)
|
||||||
|
val lockedSwitch: SwitchMaterial = layout.findViewById(R.id.waypointLocked)
|
||||||
|
|
||||||
|
builder.setTitle(title)
|
||||||
|
builder.setView(layout)
|
||||||
|
|
||||||
|
return DialogBuilder(builder, nameInput, descInput, lockedSwitch)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteMarkerDialog(id: Int) {
|
||||||
|
val waypoint = waypoints[id]
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
builder.setTitle(R.string.waypoint_delete)
|
||||||
|
builder.setNeutralButton(R.string.cancel) { _, _ ->
|
||||||
|
debug("User canceled marker delete dialog")
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(R.string.delete_for_me) { _, _ ->
|
||||||
|
debug("User deleted waypoint $id for me")
|
||||||
|
model.deleteWaypoint(id)
|
||||||
|
}
|
||||||
|
if (waypoint != null && waypoint.lockedTo in setOf(0, model.myNodeNum ?: 0))
|
||||||
|
builder.setPositiveButton(R.string.delete_for_everyone) { _, _ ->
|
||||||
|
debug("User deleted waypoint $id for everyone")
|
||||||
|
model.sendWaypoint(waypoint.copy { expire = 1 })
|
||||||
model.deleteWaypoint(id)
|
model.deleteWaypoint(id)
|
||||||
}
|
}
|
||||||
.show()
|
val dialog = builder.show()
|
||||||
|
for (button in setOf(
|
||||||
|
AlertDialog.BUTTON_NEUTRAL,
|
||||||
|
AlertDialog.BUTTON_NEGATIVE,
|
||||||
|
AlertDialog.BUTTON_POSITIVE
|
||||||
|
)) with(dialog.getButton(button)) { textSize = 12F; isAllCaps = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEditMarkerDialog(waypoint: Waypoint) {
|
||||||
|
val dialog = createEditDialog(requireContext(), getString(R.string.waypoint_edit))
|
||||||
|
dialog.nameInput.setText(waypoint.name)
|
||||||
|
dialog.descInput.setText(waypoint.description)
|
||||||
|
dialog.lockedSwitch.isEnabled = false
|
||||||
|
dialog.lockedSwitch.isChecked = waypoint.lockedTo != 0
|
||||||
|
dialog.builder.setNeutralButton(R.string.cancel) { _, _ ->
|
||||||
|
debug("User canceled marker edit dialog")
|
||||||
|
}
|
||||||
|
dialog.builder.setNegativeButton(R.string.delete) { _, _ ->
|
||||||
|
debug("User clicked delete waypoint ${waypoint.id}")
|
||||||
|
showDeleteMarkerDialog(waypoint.id)
|
||||||
|
}
|
||||||
|
dialog.builder.setPositiveButton(getString(R.string.send)) { _, _ ->
|
||||||
|
debug("User edited waypoint ${waypoint.id}")
|
||||||
|
model.sendWaypoint(waypoint.copy {
|
||||||
|
name = dialog.name.ifEmpty { return@setPositiveButton }
|
||||||
|
description = dialog.description
|
||||||
|
expire = Int.MAX_VALUE // TODO add expire picker
|
||||||
|
icon = 0 // TODO add emoji picker
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dialog.builder.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMarkerLongPressDialog(id: Int) {
|
||||||
|
debug("marker long pressed id=${id}")
|
||||||
|
val waypoint = waypoints[id]
|
||||||
|
// edit only when unlocked or lockedTo myNodeNum
|
||||||
|
if (waypoint != null && waypoint.lockedTo in setOf(0, model.myNodeNum ?: 0))
|
||||||
|
showEditMarkerDialog(waypoint)
|
||||||
|
else
|
||||||
|
showDeleteMarkerDialog(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadJobAlert() {
|
private fun downloadJobAlert() {
|
||||||
|
@ -444,12 +516,14 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging {
|
||||||
lateinit var marker: MarkerWithLabel
|
lateinit var marker: MarkerWithLabel
|
||||||
pt.data.waypoint?.let {
|
pt.data.waypoint?.let {
|
||||||
val lock = if (it.lockedTo != 0) "\uD83D\uDD12" else ""
|
val lock = if (it.lockedTo != 0) "\uD83D\uDD12" else ""
|
||||||
|
val time = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||||
|
.format(pt.received_time)
|
||||||
val label = it.name + " " + formatAgo((pt.received_time / 1000).toInt())
|
val label = it.name + " " + formatAgo((pt.received_time / 1000).toInt())
|
||||||
val emoji = String(Character.toChars(if (it.icon == 0) 128205 else it.icon))
|
val emoji = String(Character.toChars(if (it.icon == 0) 128205 else it.icon))
|
||||||
marker = MarkerWithLabel(map, label, emoji)
|
marker = MarkerWithLabel(map, label, emoji)
|
||||||
marker.id = "${it.id}"
|
marker.id = "${it.id}"
|
||||||
marker.title = "${it.name} (${getUsername(pt.data.from)}$lock)"
|
marker.title = "${it.name} (${getUsername(pt.data.from)}$lock)"
|
||||||
marker.snippet = it.description
|
marker.snippet = "[$time] " + it.description
|
||||||
marker.position = GeoPoint(it.latitudeI * 1e-7, it.longitudeI * 1e-7)
|
marker.position = GeoPoint(it.latitudeI * 1e-7, it.longitudeI * 1e-7)
|
||||||
marker.setVisible(false)
|
marker.setVisible(false)
|
||||||
}
|
}
|
||||||
|
@ -457,7 +531,7 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging {
|
||||||
}
|
}
|
||||||
return wayPoint
|
return wayPoint
|
||||||
}
|
}
|
||||||
wayPoints = getCurrentWayPoints()
|
waypointMarkers = getCurrentWayPoints()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNodesChanged(nodes: Collection<NodeInfo>) {
|
private fun onNodesChanged(nodes: Collection<NodeInfo>) {
|
||||||
|
@ -525,7 +599,7 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging {
|
||||||
addCopyright() // Copyright is required for certain map sources
|
addCopyright() // Copyright is required for certain map sources
|
||||||
createLatLongGrid(false)
|
createLatLongGrid(false)
|
||||||
map.overlayManager.addAll(nodeLayer, nodePositions)
|
map.overlayManager.addAll(nodeLayer, nodePositions)
|
||||||
map.overlayManager.addAll(nodeLayer, wayPoints)
|
map.overlayManager.addAll(nodeLayer, waypointMarkers)
|
||||||
map.overlayManager.add(nodeLayer, MapEventsOverlay(object : MapEventsReceiver {
|
map.overlayManager.add(nodeLayer, MapEventsOverlay(object : MapEventsReceiver {
|
||||||
override fun singleTapConfirmedHelper(p: GeoPoint): Boolean {
|
override fun singleTapConfirmedHelper(p: GeoPoint): Boolean {
|
||||||
InfoWindow.closeAllInfoWindowsOn(map)
|
InfoWindow.closeAllInfoWindowsOn(map)
|
||||||
|
@ -536,33 +610,24 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging {
|
||||||
performHapticFeedback()
|
performHapticFeedback()
|
||||||
if (!model.isConnected()) return true
|
if (!model.isConnected()) return true
|
||||||
|
|
||||||
val layout = LayoutInflater.from(context)
|
val dialog = createEditDialog(requireContext(), getString(R.string.waypoint_new))
|
||||||
.inflate(R.layout.dialog_add_waypoint, null)
|
dialog.builder.setNeutralButton(R.string.cancel) { _, _ ->
|
||||||
|
debug("User canceled marker create dialog")
|
||||||
val nameInput: EditText = layout.findViewById(R.id.waypointName)
|
}
|
||||||
val descriptionInput: EditText= layout.findViewById(R.id.waypointDescription)
|
dialog.builder.setPositiveButton(getString(R.string.send)) { _, _ ->
|
||||||
val lockedInput: SwitchMaterial = layout.findViewById(R.id.waypointLocked)
|
debug("User created waypoint")
|
||||||
|
model.sendWaypoint(waypoint {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
name = dialog.name.ifEmpty { return@setPositiveButton }
|
||||||
.setView(layout)
|
description = dialog.description
|
||||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
id = model.generatePacketId() ?: return@setPositiveButton
|
||||||
debug("User canceled marker create dialog")
|
latitudeI = (p.latitude * 1e7).toInt()
|
||||||
}
|
longitudeI = (p.longitude * 1e7).toInt()
|
||||||
.setPositiveButton(getString(R.string.send)) { _, _ ->
|
expire = Int.MAX_VALUE // TODO add expire picker
|
||||||
debug("User created waypoint")
|
icon = 0 // TODO add emoji picker
|
||||||
model.sendWaypoint(waypoint {
|
lockedTo = if (!dialog.lockedSwitch.isChecked) 0 else model.myNodeNum ?: 0
|
||||||
name = nameInput.text.toString().ifEmpty { return@setPositiveButton }
|
})
|
||||||
description = descriptionInput.text.toString()
|
}
|
||||||
id = model.generatePacketId() ?: return@setPositiveButton
|
dialog.builder.show()
|
||||||
latitudeI = (p.latitude * 1e7).toInt()
|
|
||||||
longitudeI = (p.longitude * 1e7).toInt()
|
|
||||||
expire = Int.MAX_VALUE // TODO add expire picker
|
|
||||||
icon = 0 // TODO add emoji picker
|
|
||||||
lockedTo = if (!lockedInput.isChecked) 0
|
|
||||||
else model.myNodeInfo.value?.myNodeNum ?: 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -111,6 +111,8 @@
|
||||||
<item quantity="other">Delete %s messages?</item>
|
<item quantity="other">Delete %s messages?</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
|
<string name="delete_for_everyone">Delete for everyone</string>
|
||||||
|
<string name="delete_for_me">Delete for me</string>
|
||||||
<string name="select_all">Select all</string>
|
<string name="select_all">Select all</string>
|
||||||
<string name="modem_config_slow_long">Long Range / Slow</string>
|
<string name="modem_config_slow_long">Long Range / Slow</string>
|
||||||
<string name="map_style_selection">Style Selection</string>
|
<string name="map_style_selection">Style Selection</string>
|
||||||
|
@ -169,4 +171,7 @@
|
||||||
<string name="map_download_errors">Download complete with %s errors</string>
|
<string name="map_download_errors">Download complete with %s errors</string>
|
||||||
<string name="map_cache_tiles">%s tiles</string>
|
<string name="map_cache_tiles">%s tiles</string>
|
||||||
<string name="map_subDescription">bearing: %1$s° distance: %2$s</string>
|
<string name="map_subDescription">bearing: %1$s° distance: %2$s</string>
|
||||||
|
<string name="waypoint_edit">Edit waypoint</string>
|
||||||
|
<string name="waypoint_delete">Delete waypoint?</string>
|
||||||
|
<string name="waypoint_new">New waypoint</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Ładowanie…
Reference in New Issue