feat: add option to show phone GPS location on map

This commit is contained in:
Andre K 2023-07-31 22:56:15 -03:00 committed by GitHub
parent a51e6afd4e
commit 377c6a18e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 192 additions and 55 deletions

View file

@ -0,0 +1,66 @@
package com.geeksville.mesh.ui.components
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import com.geeksville.mesh.R
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun IconButton(
onClick: () -> Unit,
@DrawableRes drawableRes: Int,
@StringRes contentDescription: Int,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
IconButton(
onClick = onClick,
drawableRes = drawableRes,
contentDescription = stringResource(contentDescription),
modifier = modifier,
enabled = enabled,
)
}
@Composable
fun IconButton(
onClick: () -> Unit,
@DrawableRes drawableRes: Int,
contentDescription: String?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Button(
onClick = onClick,
enabled = enabled,
modifier = modifier.size(48.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary),
) {
Icon(
painterResource(id = drawableRes),
contentDescription,
modifier = Modifier.scale(1.5f),
)
}
}
@Preview(showBackground = true)
@Composable
private fun IconButtonsPreview() {
IconButton(
onClick = {},
drawableRes = R.drawable.ic_twotone_layers_24,
R.string.map_style_selection,
)
}

View file

@ -7,7 +7,11 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold
@ -25,8 +29,10 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geeksville.mesh.BuildConfig
@ -36,6 +42,10 @@ import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.getLocationPermissions
import com.geeksville.mesh.android.gpsDisabled
import com.geeksville.mesh.android.hasGps
import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.copy
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.model.UIViewModel
@ -46,7 +56,7 @@ import com.geeksville.mesh.ui.ScreenFragment
import com.geeksville.mesh.ui.map.components.CacheLayout
import com.geeksville.mesh.ui.map.components.DownloadButton
import com.geeksville.mesh.ui.map.components.EditWaypointDialog
import com.geeksville.mesh.ui.map.components.MapStyleButton
import com.geeksville.mesh.ui.components.IconButton
import com.geeksville.mesh.util.EnableWakeLock
import com.geeksville.mesh.util.SqlTileWriterExt
import com.geeksville.mesh.util.requiredZoomLevel
@ -79,6 +89,7 @@ import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.gridlines.LatLonGridlineOverlay2
import org.osmdroid.views.overlay.infowindow.InfoWindow
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import java.io.File
import java.text.DateFormat
@ -126,6 +137,7 @@ fun MapView(model: UIViewModel = viewModel()) {
// Map Elements
var downloadRegionBoundingBox: BoundingBox? by remember { mutableStateOf(null) }
var myLocationOverlay: MyLocationNewOverlay? by remember { mutableStateOf(null) }
val context = LocalContext.current
val mPrefs = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
@ -133,6 +145,8 @@ fun MapView(model: UIViewModel = viewModel()) {
val haptic = LocalHapticFeedback.current
fun performHapticFeedback() = haptic.performHapticFeedback(HapticFeedbackType.LongPress)
val hasGps = context.hasGps()
EnableWakeLock(context)
val map = remember {
@ -140,6 +154,43 @@ fun MapView(model: UIViewModel = viewModel()) {
clipToOutline = true
}
}
fun toggleMyLocation() {
if (context.gpsDisabled()) {
debug("Telling user we need location turned on for MyLocationNewOverlay")
model.showSnackbar(R.string.location_disabled)
return
}
debug("user clicked MyLocationNewOverlay ${myLocationOverlay == null}")
if (myLocationOverlay == null) {
myLocationOverlay = MyLocationNewOverlay(map).apply {
enableMyLocation()
enableFollowLocation()
AppCompatResources.getDrawable(context, R.drawable.ic_location_dot_24)?.let {
setPersonIcon(it.toBitmap())
setPersonAnchor(0.5f, 0.5f)
}
}
map.overlays.add(myLocationOverlay)
} else {
myLocationOverlay?.apply {
disableMyLocation()
disableFollowLocation()
}
map.overlays.remove(myLocationOverlay)
myLocationOverlay = null
}
}
val requestPermissionAndToggleLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
if (permissions.entries.all { it.value }) toggleMyLocation()
}
fun requestPermissionAndToggle() {
requestPermissionAndToggleLauncher.launch(context.getLocationPermissions())
}
val nodes by model.nodeDB.nodes.observeAsState(emptyMap())
val waypoints by model.waypoints.observeAsState(emptyMap())
@ -388,6 +439,9 @@ fun MapView(model: UIViewModel = viewModel()) {
if (overlays.none { it is MapEventsOverlay }) {
overlays.add(0, MapEventsOverlay(mapEventsReceiver))
}
if (myLocationOverlay != null && overlays.none { it is MyLocationNewOverlay }) {
overlays.add(myLocationOverlay)
}
addCopyright() // Copyright is required for certain map sources
createLatLongGrid(false)
@ -601,10 +655,28 @@ fun MapView(model: UIViewModel = viewModel()) {
}
},
modifier = Modifier.align(Alignment.BottomCenter)
) else MapStyleButton(
onClick = { showMapStyleDialog() },
modifier = Modifier.align(Alignment.TopEnd),
)
) else Column(
modifier = Modifier
.padding(top = 16.dp, end = 16.dp)
.align(Alignment.TopEnd),
) {
IconButton(
onClick = { showMapStyleDialog() },
drawableRes = R.drawable.ic_twotone_layers_24,
contentDescription = R.string.map_style_selection,
)
IconButton(
onClick = {
if (context.hasLocationPermission()) toggleMyLocation()
else requestPermissionAndToggle()
},
enabled = hasGps,
drawableRes = if (myLocationOverlay == null) R.drawable.ic_twotone_my_location_24
else R.drawable.ic_twotone_location_disabled_24,
contentDescription = null,
modifier = Modifier.padding(top = 8.dp),
)
}
}
}
if (showEditWaypointDialog != null) {

View file

@ -1,44 +0,0 @@
package com.geeksville.mesh.ui.map.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import com.geeksville.mesh.R
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun MapStyleButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
modifier = modifier
.padding(16.dp)
.size(48.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
) {
Icon(
painterResource(id = R.drawable.ic_twotone_layers_24),
stringResource(R.string.map_style_selection),
modifier = Modifier.scale(1.5f)
)
}
}
@Preview(showBackground = true)
@Composable
private fun MapStyleButtonPreview() {
MapStyleButton(onClick = {})
}

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#3388ff"
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:strokeWidth="2.5"
android:strokeColor="@android:color/white" />
</vector>

View file

@ -1,8 +1,14 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="0.3"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillAlpha="0.3"
android:fillColor="@android:color/white"
android:pathData="M6.26,9L12,13.47 17.74,9 12,4.53z" android:strokeAlpha="0.3"/>
<path android:fillColor="@android:color/white" android:pathData="M19.37,12.8l-7.38,5.74 -7.37,-5.73L3,14.07l9,7 9,-7zM12,2L3,9l1.63,1.27L12,16l7.36,-5.73L21,9l-9,-7zM12,13.47L6.26,9 12,4.53 17.74,9 12,13.47z"/>
android:pathData="M6.26,9L12,13.47 17.74,9 12,4.53z"
android:strokeAlpha="0.3" />
<path
android:fillColor="@android:color/white"
android:pathData="M19.37,12.8l-7.38,5.74 -7.37,-5.73L3,14.07l9,7 9,-7zM12,2L3,9l1.63,1.27L12,16l7.36,-5.73L21,9l-9,-7zM12,13.47L6.26,9 12,4.53 17.74,9 12,13.47z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M23,13v-2h-2.06c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94V1h-2v2.06c-0.98,0.11 -1.91,0.38 -2.77,0.78l1.53,1.53C10.46,5.13 11.22,5 12,5c3.87,0 7,3.13 7,7 0,0.79 -0.13,1.54 -0.37,2.24l1.53,1.53c0.4,-0.86 0.67,-1.79 0.78,-2.77H23zM4.41,2.86L3,4.27l2.04,2.04C3.97,7.62 3.26,9.23 3.06,11H1v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94V23h2v-2.06c1.77,-0.2 3.38,-0.91 4.69,-1.98L19.73,21l1.41,-1.41L4.41,2.86zM12,19c-3.87,0 -7,-3.13 -7,-7 0,-1.61 0.55,-3.09 1.46,-4.27l9.81,9.81C15.09,18.45 13.61,19 12,19z" />
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M13,3.06V1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11H1v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94V23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94H23v-2h-2.06c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z" />
<path
android:fillAlpha="0.3"
android:fillColor="@android:color/white"
android:pathData="M12,12m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:strokeAlpha="0.3" />
<path
android:fillColor="@android:color/white"
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector>