feat: waypoints

This commit is contained in:
andrekir
2023-02-01 12:16:44 -03:00
committed by Andre K
parent a9784f3747
commit 62420132f1
10 changed files with 214 additions and 14 deletions

View File

@@ -55,6 +55,13 @@ data class DataPacket(
else
null
constructor(to: String?, channel: Int, waypoint: MeshProtos.Waypoint) : this(
to = to,
bytes = waypoint.toByteArray(),
dataType = Portnums.PortNum.WAYPOINT_APP_VALUE,
channel = channel
)
val waypoint: MeshProtos.Waypoint?
get() = if (dataType == Portnums.PortNum.WAYPOINT_APP_VALUE)
MeshProtos.Waypoint.parseFrom(bytes)
@@ -149,6 +156,7 @@ data class DataPacket(
const val NODENUM_BROADCAST = (0xffffffff).toInt()
fun nodeNumToDefaultId(n: Int): String = "!%08x".format(n)
fun idToDefaultNodeNum(id: String?): Int? = id?.toLong(16)?.toInt()
override fun createFromParcel(parcel: Parcel): DataPacket {
return DataPacket(parcel)

View File

@@ -45,6 +45,11 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
suspend fun deleteMessages(uuidList: List<Long>) = withContext(Dispatchers.IO) {
packetDao.deleteMessages(uuidList)
}
suspend fun deleteWaypoint(id: Int) = withContext(Dispatchers.IO) {
packetDao.deleteWaypoint(id)
}
suspend fun delete(packet: Packet) = withContext(Dispatchers.IO) {
packetDao.delete(packet)
}

View File

@@ -59,4 +59,13 @@ interface PacketDao {
@Transaction
fun getQueuedPackets(): List<DataPacket>? =
getDataPackets().filter { it.status in setOf(MessageStatus.ENROUTE, MessageStatus.QUEUED) }
@Query("Select * from packet where port_num = 8 order by received_time asc")
fun getAllWaypoints(): List<Packet>
@Transaction
fun deleteWaypoint(id: Int) {
val uuidList = getAllWaypoints().filter { it.data.waypoint?.id == id }.map { it.uuid }
deleteMessages(uuidList)
}
}

View File

@@ -162,12 +162,34 @@ class UIViewModel @Inject constructor(
}
}.asLiveData()
fun generatePacketId(): Int? {
return try {
meshService?.packetId
} catch (ex: RemoteException) {
errormsg("RemoteException: ${ex.message}")
return null
}
}
fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
// contactKey: unique contact key filter (channel)+(nodeId)
val channel = contactKey[0].digitToIntOrNull()
val dest = if (channel != null) contactKey.substring(1) else contactKey
val p = DataPacket(dest, channel ?: 0, str)
sendDataPacket(p)
}
fun sendWaypoint(wpt: MeshProtos.Waypoint, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
// contactKey: unique contact key filter (channel)+(nodeId)
val channel = contactKey[0].digitToIntOrNull()
val dest = if (channel != null) contactKey.substring(1) else contactKey
val p = DataPacket(dest, channel ?: 0, wpt)
if (wpt.id != 0) sendDataPacket(p.copy(id = wpt.id))
}
private fun sendDataPacket(p: DataPacket) {
try {
meshService?.send(p)
} catch (ex: RemoteException) {
@@ -195,6 +217,10 @@ class UIViewModel @Inject constructor(
packetRepository.deleteMessages(uuidList)
}
fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteWaypoint(id)
}
companion object {
fun getPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
@@ -210,7 +236,7 @@ class UIViewModel @Inject constructor(
private val _connectionState = MutableLiveData(MeshService.ConnectionState.DISCONNECTED)
val connectionState: LiveData<MeshService.ConnectionState> get() = _connectionState
fun isConnected() = _connectionState.value == MeshService.ConnectionState.CONNECTED
fun isConnected() = _connectionState.value != MeshService.ConnectionState.DISCONNECTED
fun setConnectionState(connectionState: MeshService.ConnectionState) {
_connectionState.value = connectionState

View File

@@ -1627,6 +1627,8 @@ class MeshService : Service(), Logging {
override fun getMyId() = toRemoteExceptions { myNodeID }
override fun getPacketId() = toRemoteExceptions { generatePacketId() }
override fun setOwner(myId: String?, longName: String, shortName: String, isLicensed: Boolean) =
toRemoteExceptions {
this@MeshService.setOwner(myId, longName, shortName, isLicensed)

View File

@@ -8,11 +8,19 @@ import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.os.Bundle
import android.view.*
import android.widget.*
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
@@ -23,10 +31,13 @@ import com.geeksville.mesh.model.map.CustomOverlayManager
import com.geeksville.mesh.model.map.CustomTileSource
import com.geeksville.mesh.util.SqlTileWriterExt
import com.geeksville.mesh.util.formatAgo
import com.geeksville.mesh.waypoint
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import dagger.hilt.android.AndroidEntryPoint
import org.osmdroid.api.IMapController
import org.osmdroid.config.Configuration
import org.osmdroid.events.MapEventsReceiver
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
@@ -40,8 +51,12 @@ 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.*
import org.osmdroid.views.overlay.CopyrightOverlay
import org.osmdroid.views.overlay.MapEventsOverlay
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.gridlines.LatLonGridlineOverlay2
import org.osmdroid.views.overlay.infowindow.InfoWindow
import java.io.File
import kotlin.math.log2
@@ -141,6 +156,10 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
}
}
private fun performHapticFeedback() = requireView().performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
)
private fun showCacheManagerDialog() {
val alertDialogBuilder = AlertDialog.Builder(
@@ -249,6 +268,22 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
}.start()
}
fun showMarkerLongPressDialog(id: Int) {
debug("marker long pressed id=${id}")
MaterialAlertDialogBuilder(requireContext())
.setTitle("${getString(R.string.delete)}?")
.setNeutralButton(R.string.cancel) { _, _ ->
debug("User canceled marker edit dialog")
}
// .setNegativeButton(R.string.edit) { _, _ ->
// debug("Negative button pressed") // TODO add Edit option
// }
.setPositiveButton(getString(R.string.delete)) { _, _ ->
debug("User deleted local waypoint $id")
model.deleteWaypoint(id)
}
.show()
}
private fun downloadJobAlert() {
//prompt for input params .
@@ -423,6 +458,9 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
}
}
private fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL) getString(R.string.you)
else model.nodeDB.nodes.value?.get(id)?.user?.longName ?: getString(R.string.unknown_username)
private fun onWaypointChanged(wayPt: Collection<Packet>) {
/**
@@ -430,17 +468,19 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
*/
// Find all waypoints
fun getCurrentWayPoints(): List<MarkerWithLabel> {
debug("Showing on map: ${wayPt.size} waypoints")
val wayPoint = wayPt.map { pt ->
debug("Showing on map: $pt")
lateinit var marker: MarkerWithLabel
pt.data.waypoint?.let {
val label = it.name + " " + formatAgo(it.expire)
marker = MarkerWithLabel(map, label, String(Character.toChars(it.icon)))
marker.title = it.name
val lock = if (it.lockedTo != 0) "\uD83D\uDD12" else ""
val label = it.name + " " + formatAgo((pt.received_time / 1000).toInt())
val emoji = String(Character.toChars(if (it.icon == 0) 128205 else it.icon))
marker = MarkerWithLabel(map, label, emoji)
marker.id = "${it.id}"
marker.title = "${it.name} (${getUsername(pt.data.from)}$lock)"
marker.snippet = it.description
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
marker.position = GeoPoint(it.latitudeI.toDouble(), it.longitudeI.toDouble())
marker.icon = android.graphics.drawable.ColorDrawable(Color.TRANSPARENT)
marker.position = GeoPoint(it.latitudeI * 1e-7, it.longitudeI * 1e-7)
marker.setVisible(false)
}
marker
}
@@ -516,6 +556,46 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
createLatLongGrid(false)
map.overlayManager.addAll(nodeLayer, nodePositions)
map.overlayManager.addAll(nodeLayer, wayPoints)
map.overlayManager.add(nodeLayer, MapEventsOverlay(object : MapEventsReceiver {
override fun singleTapConfirmedHelper(p: GeoPoint): Boolean {
InfoWindow.closeAllInfoWindowsOn(map)
return true
}
override fun longPressHelper(p: GeoPoint): Boolean {
performHapticFeedback()
if (!model.isConnected()) return true
val layout = LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_add_waypoint, null)
val nameInput: EditText = layout.findViewById(R.id.waypointName)
val descriptionInput: EditText= layout.findViewById(R.id.waypointDescription)
val lockedInput: SwitchMaterial = layout.findViewById(R.id.waypointLocked)
MaterialAlertDialogBuilder(requireContext())
.setView(layout)
.setNeutralButton(R.string.cancel) { _, _ ->
debug("User canceled marker create dialog")
}
.setPositiveButton(getString(R.string.save)) { _, _ ->
debug("User created waypoint")
model.sendWaypoint(waypoint {
name = nameInput.text.toString().ifEmpty { return@setPositiveButton }
description = descriptionInput.text.toString()
id = model.generatePacketId() ?: return@setPositiveButton
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
}
}))
map.invalidate()
}
@@ -671,6 +751,15 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging, View.OnClickListene
)
}
override fun onLongPress(event: MotionEvent?, mapView: MapView?): Boolean {
val touched = hitTest(event, mapView)
if (touched && this.id != null) {
performHapticFeedback()
this.id.toIntOrNull()?.run(::showMarkerLongPressDialog)
}
return super.onLongPress(event, mapView)
}
override fun draw(c: Canvas, osmv: MapView?, shadow: Boolean) {
super.draw(c, osmv, false)
val p = mPositionPixels

View File

@@ -26,7 +26,6 @@ import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
import com.geeksville.mesh.databinding.MessagesFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
@@ -298,9 +297,9 @@ class MessagesFragment : Fragment(), Logging {
}
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
model.connectionState.observe(viewLifecycleOwner) { connectionState ->
model.connectionState.observe(viewLifecycleOwner) {
// If we don't know our node ID and we are offline don't let user try to send
isConnected = connectionState != MeshService.ConnectionState.DISCONNECTED
isConnected = model.isConnected()
binding.textInputLayout.isEnabled = isConnected
binding.sendButton.isEnabled = isConnected
for (subView: View in binding.quickChatLayout.allViews) {