1717package org.meshtastic.feature.map
1818
1919import androidx.lifecycle.ViewModel
20- import androidx.lifecycle.viewModelScope
2120import kotlinx.coroutines.CoroutineDispatcher
2221import kotlinx.coroutines.flow.MutableStateFlow
2322import kotlinx.coroutines.flow.StateFlow
@@ -30,7 +29,6 @@ import org.meshtastic.core.common.util.nowSeconds
3029import org.meshtastic.core.model.DataPacket
3130import org.meshtastic.core.model.Node
3231import org.meshtastic.core.model.RadioController
33- import org.meshtastic.core.model.TracerouteOverlay
3432import org.meshtastic.core.repository.MapPrefs
3533import org.meshtastic.core.repository.NodeRepository
3634import org.meshtastic.core.repository.PacketRepository
@@ -42,7 +40,6 @@ import org.meshtastic.core.resources.one_hour
4240import org.meshtastic.core.resources.two_days
4341import org.meshtastic.core.ui.viewmodel.safeLaunch
4442import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
45- import org.meshtastic.proto.Position
4643import org.meshtastic.proto.Waypoint
4744import org.meshtastic.core.common.util.ioDispatcher as defaultIoDispatcher
4845
@@ -99,38 +96,22 @@ open class BaseMapViewModel(
9996 private val showOnlyFavorites = MutableStateFlow (mapPrefs.showOnlyFavorites.value)
10097 val showOnlyFavoritesOnMap: StateFlow <Boolean > = showOnlyFavorites.asStateFlow()
10198
102- fun toggleOnlyFavorites () {
103- val newValue = ! showOnlyFavorites.value
104- showOnlyFavorites.value = newValue
105- mapPrefs.setShowOnlyFavorites(newValue)
106- }
99+ fun toggleOnlyFavorites () = togglePref(showOnlyFavorites, mapPrefs::setShowOnlyFavorites)
107100
108101 private val showWaypoints = MutableStateFlow (mapPrefs.showWaypointsOnMap.value)
109102 val showWaypointsOnMap: StateFlow <Boolean > = showWaypoints.asStateFlow()
110103
111- fun toggleShowWaypointsOnMap () {
112- val newValue = ! showWaypoints.value
113- showWaypoints.value = newValue
114- mapPrefs.setShowWaypointsOnMap(newValue)
115- }
104+ fun toggleShowWaypointsOnMap () = togglePref(showWaypoints, mapPrefs::setShowWaypointsOnMap)
116105
117106 private val showPrecisionCircle = MutableStateFlow (mapPrefs.showPrecisionCircleOnMap.value)
118107 val showPrecisionCircleOnMap: StateFlow <Boolean > = showPrecisionCircle.asStateFlow()
119108
120- fun toggleShowPrecisionCircleOnMap () {
121- val newValue = ! showPrecisionCircle.value
122- showPrecisionCircle.value = newValue
123- mapPrefs.setShowPrecisionCircleOnMap(newValue)
124- }
109+ fun toggleShowPrecisionCircleOnMap () = togglePref(showPrecisionCircle, mapPrefs::setShowPrecisionCircleOnMap)
125110
126111 private val showWeatherRadar = MutableStateFlow (mapPrefs.showWeatherRadarOnMap.value)
127112 val showWeatherRadarOnMap: StateFlow <Boolean > = showWeatherRadar.asStateFlow()
128113
129- fun toggleShowWeatherRadarOnMap () {
130- val newValue = ! showWeatherRadar.value
131- showWeatherRadar.value = newValue
132- mapPrefs.setShowWeatherRadarOnMap(newValue)
133- }
114+ fun toggleShowWeatherRadarOnMap () = togglePref(showWeatherRadar, mapPrefs::setShowWeatherRadarOnMap)
134115
135116 private val owmApiKeyBuffer = MutableStateFlow (mapPrefs.openWeatherMapApiKey.value)
136117 val owmApiKey: StateFlow <String > = owmApiKeyBuffer.asStateFlow()
@@ -160,6 +141,13 @@ open class BaseMapViewModel(
160141
161142 fun getNodeOrFallback (nodeNum : Int ): Node = nodeRepository.nodeDBbyNum.value[nodeNum] ? : Node (num = nodeNum)
162143
144+ /* * Toggle a boolean pref flow and persist the new value. */
145+ private inline fun togglePref (flow : MutableStateFlow <Boolean >, crossinline persist : (Boolean ) -> Unit ) {
146+ val toggled = ! flow.value
147+ flow.value = toggled
148+ persist(toggled)
149+ }
150+
163151 fun deleteWaypoint (id : Int ) =
164152 safeLaunch(context = ioDispatcher, tag = " deleteWaypoint" ) { packetRepository.deleteWaypoint(id) }
165153
@@ -176,6 +164,12 @@ open class BaseMapViewModel(
176164 safeLaunch(context = ioDispatcher, tag = " sendDataPacket" ) { radioController.sendMessage(p) }
177165 }
178166
167+ private val nodeSearchQuery = MutableStateFlow (" " )
168+
169+ fun setNodeSearchQuery (query : String ) {
170+ nodeSearchQuery.value = query
171+ }
172+
179173 fun generatePacketId (): Int = radioController.getPacketId()
180174
181175 data class MapFilterState (
@@ -186,7 +180,21 @@ open class BaseMapViewModel(
186180 val owmApiKey : String ,
187181 val lastHeardFilter : LastHeardFilter ,
188182 val lastHeardTrackFilter : LastHeardFilter ,
189- )
183+ val nodeSearchQuery : String = " " ,
184+ ) {
185+ /* * Number of active non-default filters, shown as a badge on the filter button. */
186+ val activeFilterCount: Int
187+ get() =
188+ listOf (
189+ onlyFavorites,
190+ ! showWaypoints,
191+ ! showPrecisionCircle,
192+ showWeatherRadar,
193+ lastHeardFilter != LastHeardFilter .Any ,
194+ nodeSearchQuery.isNotBlank(),
195+ )
196+ .count { it }
197+ }
190198
191199 val mapFilterStateFlow: StateFlow <MapFilterState > =
192200 combine(
@@ -209,8 +217,9 @@ open class BaseMapViewModel(
209217 },
210218 showWeatherRadarOnMap,
211219 owmApiKey,
212- ) { state, weatherRadar, apiKey ->
213- state.copy(showWeatherRadar = weatherRadar, owmApiKey = apiKey)
220+ nodeSearchQuery,
221+ ) { state, weatherRadar, apiKey, searchQuery ->
222+ state.copy(showWeatherRadar = weatherRadar, owmApiKey = apiKey, nodeSearchQuery = searchQuery)
214223 }
215224 .stateInWhileSubscribed(
216225 initialValue =
@@ -225,89 +234,27 @@ open class BaseMapViewModel(
225234 ),
226235 )
227236
228- /* * Nodes with position, filtered by favorites and last-heard preferences . */
237+ /* * Nodes with position, filtered by favorites, last-heard, and search query . */
229238 val filteredNodes: StateFlow <List <Node >> =
230239 combine(nodesWithPosition, mapFilterStateFlow) { nodes, filter ->
231240 val myNum = myNodeNum
232- nodes
233- .filter { node -> ! filter.onlyFavorites || node.isFavorite || node.num == myNum }
234- .filter { node ->
241+ val query = filter.nodeSearchQuery.trim().lowercase()
242+ nodes.filter { node ->
243+ val isMyNode = node.num == myNum
244+ val matchesFavorite = ! filter.onlyFavorites || node.isFavorite || isMyNode
245+ val matchesLastHeard =
235246 filter.lastHeardFilter.seconds == 0L ||
236247 (nowSeconds - node.lastHeard) <= filter.lastHeardFilter.seconds ||
237- node.num == myNum
238- }
239- }
240- .stateInWhileSubscribed(initialValue = emptyList())
241- }
242-
243- /* *
244- * Result of resolving a [TracerouteOverlay]'s node nums into displayable [Node] instances.
245- *
246- * @property overlayNodeNums All unique node nums referenced by the traceroute.
247- * @property nodesForMarkers Nodes to render as map markers (with snapshot positions when available).
248- * @property nodeLookup Node-num-keyed map for polyline coordinate resolution.
249- */
250- internal data class TracerouteNodeSelection (
251- val overlayNodeNums : Set <Int >,
252- val nodesForMarkers : List <Node >,
253- val nodeLookup : Map <Int , Node >,
254- )
255-
256- /* * Convenience extension that delegates to [tracerouteNodeSelection] using the VM's [getNodeOrFallback]. */
257- internal fun BaseMapViewModel.tracerouteNodeSelection (
258- tracerouteOverlay : TracerouteOverlay ? ,
259- tracerouteNodePositions : Map <Int , Position >,
260- nodes : List <Node >,
261- ): TracerouteNodeSelection = tracerouteNodeSelection(
262- tracerouteOverlay = tracerouteOverlay,
263- tracerouteNodePositions = tracerouteNodePositions,
264- nodes = nodes,
265- getNodeOrFallback = ::getNodeOrFallback,
266- )
267-
268- /* *
269- * Resolves traceroute overlay node nums into displayable [Node] instances. Snapshot positions (recorded at traceroute
270- * time) take priority over live positions from the node database.
271- *
272- * @param getNodeOrFallback Provides a [Node] for a given num, falling back to a stub if not in the DB.
273- */
274- internal fun tracerouteNodeSelection (
275- tracerouteOverlay : TracerouteOverlay ? ,
276- tracerouteNodePositions : Map <Int , Position >,
277- nodes : List <Node >,
278- getNodeOrFallback : (Int ) -> Node ,
279- ): TracerouteNodeSelection {
280- val overlayNodeNums = tracerouteOverlay?.relatedNodeNums ? : emptySet()
281- val tracerouteSnapshotNodes =
282- if (tracerouteOverlay == null || tracerouteNodePositions.isEmpty()) {
283- emptyList()
284- } else {
285- tracerouteNodePositions.map { (nodeNum, position) -> getNodeOrFallback(nodeNum).copy(position = position) }
286- }
287-
288- val nodesForMarkers =
289- if (tracerouteOverlay != null ) {
290- if (tracerouteSnapshotNodes.isNotEmpty()) {
291- tracerouteSnapshotNodes.filter { overlayNodeNums.contains(it.num) }
292- } else {
293- nodes.filter { overlayNodeNums.contains(it.num) }
248+ isMyNode
249+ val matchesSearch =
250+ query.isEmpty() ||
251+ node.user.short_name.lowercase().contains(query) ||
252+ node.user.long_name.lowercase().contains(query) ||
253+ isMyNode
254+ matchesFavorite && matchesLastHeard && matchesSearch
294255 }
295- } else {
296- nodes
297256 }
298-
299- val nodesForLookup =
300- if (tracerouteSnapshotNodes.isNotEmpty()) {
301- tracerouteSnapshotNodes
302- } else {
303- nodes.filter { it.validPosition != null }
304- }
305-
306- return TracerouteNodeSelection (
307- overlayNodeNums = overlayNodeNums,
308- nodesForMarkers = nodesForMarkers,
309- nodeLookup = nodesForLookup.associateBy { it.num },
310- )
257+ .stateInWhileSubscribed(initialValue = emptyList())
311258}
312259
313260@Suppress(" MagicNumber" )
0 commit comments