feat: waypoints

master
andrekir 2023-02-01 12:16:44 -03:00 zatwierdzone przez Andre K
rodzic a9784f3747
commit 62420132f1
10 zmienionych plików z 214 dodań i 14 usunięć

Wyświetl plik

@ -61,6 +61,9 @@ interface IMeshService {
/// Return my unique user ID string
String getMyId();
/// Return a unique packet ID
int getPacketId();
/*
Send a packet to a specified node name

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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) {

Wyświetl plik

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp">
<EditText
android:id="@+id/waypointName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/name"
android:inputType="textShortMessage"
android:minHeight="48dp" />
<EditText
android:id="@+id/waypointDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/description"
android:inputType="textMultiLine"
android:minHeight="48dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageLock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/sl_lock_24dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/waypointLocked"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/locked"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageLock"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

Wyświetl plik

@ -18,8 +18,10 @@
<string name="connection_status">Connection status</string>
<string name="application_icon">application icon</string>
<string name="unknown_username">Unknown Username</string>
<string name="send">Send</string>
<string name="send_text">Send Text</string>
<string name="warning_not_paired">You haven\'t yet paired a Meshtastic compatible radio with this phone. Please pair a device and set your username.\n\nThis open-source application is in development, if you find problems please post on our forum: meshtastic.discourse.group.\n\nFor more information see our web page - www.meshtastic.org.</string>
<string name="you">You</string>
<string name="your_name">Your Name</string>
<string name="analytics_okay">Anonymous usage statistics and crash reports.</string>
<string name="looking_for_meshtastic_devices">Looking for Meshtastic devices…</string>
@ -114,6 +116,8 @@
<string name="map_style_selection">Style Selection</string>
<string name="map_download_region">Download Region</string>
<string name="name">Name</string>
<string name="description">Description</string>
<string name="locked">Locked</string>
<string name="save">Save</string>
<string name="preferences_language">Language</string>
<string name="preferences_system_default">System default</string>