diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index a53500e64..5cc46bc67 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -477,7 +477,7 @@ constructor( dataPacket.alert ?: getString(Res.string.critical_alert), ) } else if (updateNotification) { - scope.handledLaunch { updateMessageNotification(contactKey, dataPacket) } + scope.handledLaunch { updateNotification(contactKey, dataPacket) } } } } @@ -487,30 +487,37 @@ constructor( private fun getSenderName(packet: DataPacket): String = nodeManager.nodeDBbyID[packet.from]?.user?.longName ?: getString(Res.string.unknown_username) - private suspend fun updateMessageNotification(contactKey: String, dataPacket: DataPacket) { - val message = - when (dataPacket.dataType) { - Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> dataPacket.text!! - Portnums.PortNum.WAYPOINT_APP_VALUE -> - getString(Res.string.waypoint_received, dataPacket.waypoint!!.name) - - else -> return + private suspend fun updateNotification(contactKey: String, dataPacket: DataPacket) { + when (dataPacket.dataType) { + Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> { + val message = dataPacket.text!! + val channelName = + if (dataPacket.to == DataPacket.ID_BROADCAST) { + radioConfigRepository.channelSetFlow.first().settingsList.getOrNull(dataPacket.channel)?.name + } else { + null + } + serviceNotifications.updateMessageNotification( + contactKey, + getSenderName(dataPacket), + message, + dataPacket.to == DataPacket.ID_BROADCAST, + channelName, + ) } - val channelName = - if (dataPacket.to == DataPacket.ID_BROADCAST) { - radioConfigRepository.channelSetFlow.first().settingsList.getOrNull(dataPacket.channel)?.name - } else { - null + Portnums.PortNum.WAYPOINT_APP_VALUE -> { + val message = getString(Res.string.waypoint_received, dataPacket.waypoint!!.name) + serviceNotifications.updateWaypointNotification( + contactKey, + getSenderName(dataPacket), + message, + dataPacket.waypoint!!.id, + ) } - serviceNotifications.updateMessageNotification( - contactKey, - getSenderName(dataPacket), - message, - dataPacket.to == DataPacket.ID_BROADCAST, - channelName, - ) + else -> return + } } private fun rememberReaction(packet: MeshPacket) = scope.handledLaunch { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt index b56fc0791..223d6505e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.geeksville.mesh.service import android.app.Notification @@ -55,6 +54,7 @@ import org.meshtastic.core.strings.meshtastic_low_battery_temporary_remote_notif import org.meshtastic.core.strings.meshtastic_messages_notifications import org.meshtastic.core.strings.meshtastic_new_nodes_notifications import org.meshtastic.core.strings.meshtastic_service_notifications +import org.meshtastic.core.strings.meshtastic_waypoints_notifications import org.meshtastic.core.strings.new_node_seen import org.meshtastic.core.strings.no_local_stats import org.meshtastic.core.strings.reply @@ -111,6 +111,13 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva NotificationManager.IMPORTANCE_DEFAULT, ) + object Waypoint : + NotificationType( + "my_waypoints", + Res.string.meshtastic_waypoints_notifications, + NotificationManager.IMPORTANCE_DEFAULT, + ) + object Alert : NotificationType( "my_alerts", @@ -152,6 +159,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva ServiceState, DirectMessage, BroadcastMessage, + Waypoint, Alert, NewNode, LowBatteryLocal, @@ -190,6 +198,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva NotificationType.DirectMessage, NotificationType.BroadcastMessage, + NotificationType.Waypoint, NotificationType.NewNode, NotificationType.LowBatteryLocal, NotificationType.LowBatteryRemote, @@ -283,6 +292,11 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva notificationManager.notify(contactKey.hashCode(), notification) } + override fun updateWaypointNotification(contactKey: String, name: String, message: String, waypointId: Int) { + val notification = createWaypointNotification(name, message, waypointId) + notificationManager.notify(contactKey.hashCode(), notification) + } + override fun showAlertNotification(contactKey: String, name: String, alert: String) { val notification = createAlertNotification(contactKey, name, alert) // Use a consistent, unique ID for each alert source. @@ -373,6 +387,20 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva return builder.build() } + private fun createWaypointNotification(name: String, message: String, waypointId: Int): Notification { + val person = Person.Builder().setName(name).build() + val style = NotificationCompat.MessagingStyle(person).addMessage(message, System.currentTimeMillis(), person) + + return commonBuilder(NotificationType.Waypoint, createOpenWaypointIntent(waypointId)) + .setCategory(Notification.CATEGORY_MESSAGE) + .setAutoCancel(true) + .setStyle(style) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .build() + } + private fun createAlertNotification(contactKey: String, name: String, alert: String): Notification { val person = Person.Builder().setName(name).build() val style = NotificationCompat.MessagingStyle(person).addMessage(alert, System.currentTimeMillis(), person) @@ -454,6 +482,19 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva } } + private fun createOpenWaypointIntent(waypointId: Int): PendingIntent { + val deepLinkUri = "$DEEP_LINK_BASE_URI/map?waypointId=$waypointId".toUri() + val deepLinkIntent = + Intent(Intent.ACTION_VIEW, deepLinkUri, context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + return TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(deepLinkIntent) + getPendingIntent(waypointId, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + } + } + private fun createReplyAction(contactKey: String): NotificationCompat.Action { val replyLabel = getString(Res.string.reply) val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build() diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index ea07f4f46..26e910ee8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - @file:Suppress("MatchingDeclarationName") package com.geeksville.mesh.ui @@ -161,7 +160,7 @@ import org.meshtastic.proto.MeshProtos enum class TopLevelDestination(val label: StringResource, val icon: ImageVector, val route: Route) { Conversations(Res.string.conversations, MeshtasticIcons.Conversations, ContactsRoutes.ContactsGraph), Nodes(Res.string.nodes, MeshtasticIcons.Nodes, NodesRoutes.NodesGraph), - Map(Res.string.map, MeshtasticIcons.Map, MapRoutes.Map), + Map(Res.string.map, MeshtasticIcons.Map, MapRoutes.Map()), Settings(Res.string.bottom_nav_settings, MeshtasticIcons.Settings, SettingsRoutes.SettingsGraph()), Connections(Res.string.connections, Icons.Rounded.Wifi, ConnectionsRoutes.ConnectionsGraph), ; diff --git a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt index 1727762bb..8e1f784b3 100644 --- a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt +++ b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt @@ -89,6 +89,8 @@ class FakeMeshServiceNotifications : MeshServiceNotifications { channelName: String?, ) {} + override fun updateWaypointNotification(contactKey: String, name: String, message: String, waypointId: Int) {} + override fun showAlertNotification(contactKey: String, name: String, alert: String) {} override fun showNewNodeSeenNotification(node: NodeEntity) {} diff --git a/core/navigation/src/main/kotlin/org/meshtastic/core/navigation/Routes.kt b/core/navigation/src/main/kotlin/org/meshtastic/core/navigation/Routes.kt index 343c15ea1..b7a923c66 100644 --- a/core/navigation/src/main/kotlin/org/meshtastic/core/navigation/Routes.kt +++ b/core/navigation/src/main/kotlin/org/meshtastic/core/navigation/Routes.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.navigation import kotlinx.serialization.Serializable @@ -50,7 +49,7 @@ object ContactsRoutes { } object MapRoutes { - @Serializable data object Map : Route + @Serializable data class Map(val waypointId: Int? = null) : Route } object NodesRoutes { diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt index 14dc2e17d..54ce8031a 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.service import android.app.Notification @@ -24,6 +23,7 @@ import org.meshtastic.proto.TelemetryProtos const val SERVICE_NOTIFY_ID = 101 +@Suppress("TooManyFunctions") interface MeshServiceNotifications { fun clearNotifications() @@ -39,6 +39,8 @@ interface MeshServiceNotifications { channelName: String?, ) + fun updateWaypointNotification(contactKey: String, name: String, message: String, waypointId: Int) + fun showAlertNotification(contactKey: String, name: String, alert: String) fun showNewNodeSeenNotification(node: NodeEntity) diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index 2d3f112d8..ab66293c0 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -256,6 +256,7 @@ New messages below Direct message notifications Broadcast message notifications + Waypoint notifications Alert notifications Firmware update required. The radio firmware is too old to talk to this application. For more information on this see our Firmware Installation guide. diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt index 606004375..e87821579 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.map import android.Manifest // Added for Accompanist @@ -336,6 +335,18 @@ fun MapView( val nodes by mapViewModel.nodes.collectAsStateWithLifecycle() val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap()) + val selectedWaypointId by mapViewModel.selectedWaypointId.collectAsStateWithLifecycle() + + LaunchedEffect(selectedWaypointId, waypoints) { + if (selectedWaypointId != null && waypoints.containsKey(selectedWaypointId)) { + waypoints[selectedWaypointId]?.data?.waypoint?.let { pt -> + val geoPoint = GeoPoint(pt.latitudeI * 1e-7, pt.longitudeI * 1e-7) + map.controller.setCenter(geoPoint) + map.controller.setZoom(WAYPOINT_ZOOM) + } + } + } + val tracerouteSelection = remember(tracerouteOverlay, tracerouteNodePositions, nodes) { mapViewModel.tracerouteNodeSelection( @@ -502,7 +513,7 @@ fun MapView( } @Suppress("MagicNumber") - fun MapView.onWaypointChanged(waypoints: Collection): List { + fun MapView.onWaypointChanged(waypoints: Collection, selectedWaypointId: Int?): List { val dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) return waypoints.mapNotNull { waypoint -> val pt = waypoint.data.waypoint ?: return@mapNotNull null @@ -540,7 +551,9 @@ fun MapView( com.meshtastic.core.strings.getString(Res.string.expires) + ": $expireTimeStr" position = GeoPoint(pt.latitudeI * 1e-7, pt.longitudeI * 1e-7) - setVisible(false) // This seems to be always false, was this intended? + if (selectedWaypointId == pt.id) { + showInfoWindow() + } setOnLongClickListener { showMarkerLongPressDialog(pt.id) true @@ -716,7 +729,7 @@ fun MapView( with(mapView) { updateMarkers( onNodesChanged(nodesForMarkers), - onWaypointChanged(waypoints.values), + onWaypointChanged(waypoints.values, selectedWaypointId), nodeClusterer, ) } @@ -1082,6 +1095,7 @@ private const val EARTH_RADIUS_METERS = 6_371_000.0 private const val TRACEROUTE_OFFSET_METERS = 100.0 private const val TRACEROUTE_SINGLE_POINT_ZOOM = 12.0 private const val TRACEROUTE_ZOOM_OUT_LEVELS = 0.5 +private const val WAYPOINT_ZOOM = 15.0 private fun Double.toRad(): Double = Math.toRadians(this) diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 93e5c03bf..2ae3ac387 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,21 +14,27 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.map +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.toRoute import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalOnlyProtos.LocalConfig import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel class MapViewModel @Inject @@ -39,8 +45,12 @@ constructor( serviceRepository: ServiceRepository, radioConfigRepository: RadioConfigRepository, buildConfigProvider: BuildConfigProvider, + savedStateHandle: SavedStateHandle, ) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { + private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute().waypointId) + val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() + var mapStyleId: Int get() = mapPrefs.mapStyle set(value) { diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt index d19568b17..5494020ed 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - @file:Suppress("MagicNumber") package org.meshtastic.feature.map @@ -263,6 +262,8 @@ fun MapView( .collectAsStateWithLifecycle(listOf()) val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap()) val displayableWaypoints = waypoints.values.mapNotNull { it.data.waypoint } + val selectedWaypointId by mapViewModel.selectedWaypointId.collectAsStateWithLifecycle() + val tracerouteSelection = remember(tracerouteOverlay, tracerouteNodePositions, allNodes) { mapViewModel.tracerouteNodeSelection( @@ -581,6 +582,7 @@ fun MapView( isConnected = isConnected, unicodeEmojiToBitmapProvider = ::unicodeEmojiToBitmap, onEditWaypointRequest = { waypointToEdit -> editingWaypoint = waypointToEdit }, + selectedWaypointId = selectedWaypointId, ) MapEffect(mapLayers) { map -> diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 6aaaa4b55..3e4a19d18 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,13 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.map import android.app.Application import android.net.Uri import androidx.core.net.toFile +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute import co.touchlab.kermit.Logger import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.model.CameraPosition @@ -52,6 +53,7 @@ import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.datastore.UiPreferencesDataSource +import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.service.ServiceRepository @@ -91,8 +93,12 @@ constructor( serviceRepository: ServiceRepository, private val customTileProviderRepository: CustomTileProviderRepository, uiPreferencesDataSource: UiPreferencesDataSource, + savedStateHandle: SavedStateHandle, ) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { + private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute().waypointId) + val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() + private val targetLatLng = googleMapsPrefs.cameraTargetLat .takeIf { it != 0.0 } @@ -258,6 +264,17 @@ constructor( loadPersistedMapType() } loadPersistedLayers() + + selectedWaypointId.value?.let { wpId -> + viewModelScope.launch { + val wpMap = waypoints.first { it.containsKey(wpId) } + wpMap[wpId]?.let { packet -> + val waypoint = packet.data.waypoint!! + val latLng = LatLng(waypoint.latitudeI / 1e7, waypoint.longitudeI / 1e7) + cameraPositionState.position = CameraPosition.fromLatLngZoom(latLng, 15f) + } + } + } } fun saveCameraPosition(cameraPosition: CameraPosition) { diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt index 89ae93a40..b5a376e10 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,10 +14,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.map.component import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import com.google.android.gms.maps.model.BitmapDescriptor @@ -41,6 +41,7 @@ fun WaypointMarkers( isConnected: Boolean, unicodeEmojiToBitmapProvider: (Int) -> BitmapDescriptor, onEditWaypointRequest: (MeshProtos.Waypoint) -> Unit, + selectedWaypointId: Int? = null, ) { val scope = rememberCoroutineScope() val context = LocalContext.current @@ -49,6 +50,12 @@ fun WaypointMarkers( val markerState = rememberUpdatedMarkerState(position = LatLng(waypoint.latitudeI * DEG_D, waypoint.longitudeI * DEG_D)) + LaunchedEffect(selectedWaypointId) { + if (selectedWaypointId == waypoint.id) { + markerState.showInfoWindow() + } + } + Marker( state = markerState, icon =