Skip to content

Commit

Permalink
Add coverage on map
Browse files Browse the repository at this point in the history
Signed-off-by: Simó Albert i Beltran <sim6@probeta.net>
  • Loading branch information
sim6 committed Jan 31, 2025
1 parent 22860a4 commit e833de1
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 36 deletions.
6 changes: 6 additions & 0 deletions app/src/main/java/xyz/malkki/neostumbler/CoverageParams.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package xyz.malkki.neostumbler

data class CoverageParams(
val tileJson: String?,
val layerId: String?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ object PreferenceKeys {
const val GEOSUBMIT_PATH = "geosubmit_path"
const val GEOSUBMIT_API_KEY = "geosubmit_api_key"

const val COVERAGE_TILE_JSON = "coverage_tile_json"
const val COVERAGE_LAYER_ID = "coverage_layer_id"

const val MOVEMENT_DETECTOR = "movement_detector"

const val SCANNER_NOTIFICATION_STYLE = "scanner_notification_style"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import xyz.malkki.neostumbler.CoverageParams
import xyz.malkki.neostumbler.R
import xyz.malkki.neostumbler.StumblerApplication
import xyz.malkki.neostumbler.constants.PreferenceKeys
Expand All @@ -49,39 +50,63 @@ private fun DataStore<Preferences>.geosubmitParams(): Flow<GeosubmitParams> = da
}
.distinctUntilChanged()

private fun DataStore<Preferences>.coverageParams(): Flow<CoverageParams> = data
.map { preferences ->
val tileJson = preferences[stringPreferencesKey(PreferenceKeys.COVERAGE_TILE_JSON)]
val layerId = preferences[stringPreferencesKey(PreferenceKeys.COVERAGE_LAYER_ID)]

CoverageParams(tileJson, layerId)
}
.distinctUntilChanged()

@Composable
fun GeosubmitEndpointSettings() {
val context = LocalContext.current

val coroutineScope = rememberCoroutineScope()

val settingsStore = (context.applicationContext as StumblerApplication).settingsStore
val params = settingsStore.geosubmitParams().collectAsState(initial = null)
val geosubmitParams = settingsStore.geosubmitParams().collectAsState(initial = null)
val coverageParams = settingsStore.coverageParams().collectAsState(initial = null)

val dialogOpen = rememberSaveable { mutableStateOf(false) }

if (dialogOpen.value) {
GeosubmitEndpointDialog(
currentParams = params.value,
onDialogClose = { newParams ->
if (newParams != null) {
coroutineScope.launch {
currentGeosubmitParams = geosubmitParams.value,
currentCoverageParams = coverageParams.value,
onDialogClose = { newGeosubmitParams, newCoverageParams ->
coroutineScope.launch {
if (newGeosubmitParams != null) {
settingsStore.updateData { prefs ->
prefs.toMutablePreferences().apply {
set(stringPreferencesKey(PreferenceKeys.GEOSUBMIT_ENDPOINT), newParams.baseUrl)
set(stringPreferencesKey(PreferenceKeys.GEOSUBMIT_PATH), newParams.path)
set(stringPreferencesKey(PreferenceKeys.GEOSUBMIT_ENDPOINT), newGeosubmitParams.baseUrl)
set(stringPreferencesKey(PreferenceKeys.GEOSUBMIT_PATH), newGeosubmitParams.path)

if (newParams.apiKey != null) {
set(stringPreferencesKey(PreferenceKeys.GEOSUBMIT_API_KEY), newParams.apiKey)
if (newGeosubmitParams.apiKey != null) {
set(stringPreferencesKey(PreferenceKeys.GEOSUBMIT_API_KEY), newGeosubmitParams.apiKey)
} else {
remove(stringPreferencesKey(PreferenceKeys.GEOSUBMIT_API_KEY))
}
}
}

dialogOpen.value = false
}
} else {
if (newCoverageParams != null) {
settingsStore.updateData { prefs ->
prefs.toMutablePreferences().apply {
if (newCoverageParams.tileJson != null) {
set(stringPreferencesKey(PreferenceKeys.COVERAGE_TILE_JSON), newCoverageParams.tileJson)
} else {
remove(stringPreferencesKey(PreferenceKeys.COVERAGE_TILE_JSON))
}
if (newCoverageParams.layerId != null) {
set(stringPreferencesKey(PreferenceKeys.COVERAGE_LAYER_ID), newCoverageParams.layerId)
} else {
remove(stringPreferencesKey(PreferenceKeys.COVERAGE_LAYER_ID))
}
}
}
}
dialogOpen.value = false
}
}
Expand All @@ -90,23 +115,29 @@ fun GeosubmitEndpointSettings() {

SettingsItem(
title = stringResource(R.string.endpoint),
description = params.value?.baseUrl ?: "",
description = geosubmitParams.value?.baseUrl ?: "",
onClick = {
dialogOpen.value = true
}
)
}

@Composable
private fun GeosubmitEndpointDialog(currentParams: GeosubmitParams?, onDialogClose: (GeosubmitParams?) -> Unit) {
private fun GeosubmitEndpointDialog(currentGeosubmitParams: GeosubmitParams?, currentCoverageParams: CoverageParams?, onDialogClose: (GeosubmitParams?, CoverageParams?) -> Unit) {
val endpoint = rememberSaveable {
mutableStateOf(currentParams?.baseUrl)
mutableStateOf(currentGeosubmitParams?.baseUrl)
}
val path = rememberSaveable {
mutableStateOf(currentParams?.path)
mutableStateOf(currentGeosubmitParams?.path)
}
val apiKey = rememberSaveable {
mutableStateOf(currentParams?.apiKey)
mutableStateOf(currentGeosubmitParams?.apiKey)
}
val tileJson = rememberSaveable {
mutableStateOf(currentCoverageParams?.tileJson)
}
val layerId = rememberSaveable {
mutableStateOf(currentCoverageParams?.layerId)
}

val showSuggestedServicesDialog = rememberSaveable {
Expand All @@ -120,6 +151,8 @@ private fun GeosubmitEndpointDialog(currentParams: GeosubmitParams?, onDialogClo
endpoint.value = service.endpoint.baseUrl
path.value = service.endpoint.path
apiKey.value = service.endpoint.apiKey
tileJson.value = service.coverage.tileJson
layerId.value = service.coverage.layerId
}

showSuggestedServicesDialog.value = false
Expand All @@ -128,7 +161,7 @@ private fun GeosubmitEndpointDialog(currentParams: GeosubmitParams?, onDialogClo
}

BasicAlertDialog(
onDismissRequest = { onDialogClose(null) }
onDismissRequest = { onDialogClose(null, null) }
) {
Surface(
modifier = Modifier
Expand Down Expand Up @@ -186,6 +219,34 @@ private fun GeosubmitEndpointDialog(currentParams: GeosubmitParams?, onDialogClo

Spacer(modifier = Modifier.height(24.dp))

TextField(
modifier = Modifier.fillMaxWidth(),
value = tileJson.value ?: "",
onValueChange = { newTileJson ->
tileJson.value = newTileJson
},
label = { Text(text = stringResource(id = R.string.tile_json)) },
singleLine = true
)

if (tileJson.value.isUnencryptedUrl) {
UnencryptedEndpointWarning()
}

Spacer(modifier = Modifier.height(8.dp))

TextField(
modifier = Modifier.fillMaxWidth(),
value = layerId.value ?: "",
onValueChange = { newLayerId ->
layerId.value = newLayerId
},
label = { Text(text = stringResource(id = R.string.layer_id)) },
singleLine = true
)

Spacer(modifier = Modifier.height(8.dp))

Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
Expand All @@ -201,7 +262,10 @@ private fun GeosubmitEndpointDialog(currentParams: GeosubmitParams?, onDialogClo
Spacer(modifier = Modifier.weight(1.0f))

TextButton(
onClick = { onDialogClose(GeosubmitParams(endpoint.value!!, path.value!!, apiKey.value)) },
onClick = { onDialogClose(
GeosubmitParams(endpoint.value!!, path.value!!, apiKey.value),
CoverageParams(tileJson.value, layerId.value)
)},
enabled = !endpoint.value.isNullOrBlank() && !path.value.isNullOrBlank()
) {
Text(text = stringResource(id = R.string.save))
Expand Down
69 changes: 54 additions & 15 deletions app/src/main/java/xyz/malkki/neostumbler/ui/screens/MapScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -61,6 +62,10 @@ import org.maplibre.android.maps.Style
import org.maplibre.android.module.http.HttpRequestUtil
import org.maplibre.android.plugins.annotation.FillManager
import org.maplibre.android.plugins.annotation.FillOptions
import org.maplibre.android.style.layers.FillLayer
import org.maplibre.android.style.layers.PropertyFactory
import org.maplibre.android.style.sources.VectorSource
import xyz.malkki.neostumbler.CoverageParams
import xyz.malkki.neostumbler.R
import xyz.malkki.neostumbler.extensions.checkMissingPermissions
import xyz.malkki.neostumbler.ui.composables.KeepScreenOn
Expand All @@ -70,9 +75,12 @@ import xyz.malkki.neostumbler.ui.viewmodel.MapViewModel.MapTileSource

private val HEAT_LOW = ColorUtils.setAlphaComponent(0xd278ff, 120)
private val HEAT_HIGH = ColorUtils.setAlphaComponent(0xaa00ff, 120)
private val COVERAGE_COLOR = "#20bbc9"
private val COVERAGE_OPACITY = 0.4f

@Composable
fun MapScreen(mapViewModel: MapViewModel = viewModel()) {

val context = LocalContext.current

val lifecycle = LocalLifecycleOwner.current.lifecycle
Expand All @@ -97,6 +105,12 @@ fun MapScreen(mapViewModel: MapViewModel = viewModel()) {

val mapStyle = mapViewModel.mapStyle.collectAsState(initial = null)

val tileJson = mapViewModel.tileJson.collectAsState(initial = null)

val layerId = mapViewModel.layerId.collectAsState(initial = null)

val getCoverageParams: () -> CoverageParams = { CoverageParams(tileJson.value, layerId.value) }

val latestReportPosition = mapViewModel.latestReportPosition.collectAsState(initial = null)

val heatMapTiles = mapViewModel.heatMapTiles.collectAsState(initial = emptyList())
Expand Down Expand Up @@ -171,15 +185,7 @@ fun MapScreen(mapViewModel: MapViewModel = viewModel()) {
}
})

val styleBuilder = Style.Builder().apply {
if (mapStyle.value!!.styleJson != null) {
fromJson(mapStyle.value!!.styleJson!!)
} else {
fromUri(mapStyle.value!!.styleUrl!!)
}
}

map.setStyle(styleBuilder) { style ->
map.setStyle(styleBuilder(mapStyle, getCoverageParams())) { style ->
map.locationComponent.activateLocationComponent(
LocationComponentActivationOptions.builder(context, style)
//Set location engine to null, because we provide locations by ourself
Expand Down Expand Up @@ -225,12 +231,15 @@ fun MapScreen(mapViewModel: MapViewModel = viewModel()) {

//Ugly, but we don't want to update the map style unless it has actually changed
//TODO: think about a better way to do this
if (map.style != null) {
if (mapStyle.value!!.styleUrl != null && map.style!!.uri != mapStyle.value!!.styleUrl) {
map.setStyle(Style.Builder().fromUri(mapStyle.value!!.styleUrl!!))
} else if (mapStyle.value!!.styleJson != null && map.style!!.json != mapStyle.value!!.styleJson) {
map.setStyle(Style.Builder().fromJson(mapStyle.value!!.styleJson!!))
}
if (
map.style != null
&& (
mapStyle.value!!.styleUrl != null && map.style!!.uri != mapStyle.value!!.styleUrl
||
mapStyle.value!!.styleJson != null && map.style!!.json != mapStyle.value!!.styleJson
)
) {
map.setStyle(styleBuilder(mapStyle, getCoverageParams()))
}
}

Expand Down Expand Up @@ -279,6 +288,36 @@ fun MapScreen(mapViewModel: MapViewModel = viewModel()) {
}
}

private fun styleBuilder(mapStyle: State<MapViewModel.MapStyle?>, coverageParams: CoverageParams): Style.Builder {
val builder = Style.Builder().apply {
if (mapStyle.value!!.styleJson != null) {
fromJson(mapStyle.value!!.styleJson!!)
} else {
fromUri(mapStyle.value!!.styleUrl!!)
}
}

if ( coverageParams.tileJson != null ) {
val layer = FillLayer("coverage-layer", "coverage-source")
.withProperties(
PropertyFactory.fillColor(COVERAGE_COLOR),
PropertyFactory.fillOpacity(COVERAGE_OPACITY)
)
if ( coverageParams.layerId != null ) {
layer.setSourceLayer(coverageParams.layerId)
}
builder.withSource(
VectorSource(
"coverage-source",
coverageParams.tileJson
)
)
.withLayer(layer)
}

return builder
}

private fun createHeatMapFill(tiles: Collection<MapViewModel.HeatMapTile>): List<FillOptions> {
return tiles.map { tile ->
val color = ColorUtils.blendARGB(HEAT_LOW, HEAT_HIGH, tile.heatPct)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
}
.shareIn(viewModelScope, started = SharingStarted.Eagerly, replay = 1)

val tileJson: Flow<String?> = settingsStore.data
.map { prefs ->
prefs.get(stringPreferencesKey(PreferenceKeys.COVERAGE_TILE_JSON))
}

val layerId: Flow<String?> = settingsStore.data
.map { prefs ->
prefs.get(stringPreferencesKey(PreferenceKeys.COVERAGE_LAYER_ID))
}

private val showMyLocation = MutableStateFlow(getApplication<StumblerApplication>().checkMissingPermissions(Manifest.permission.ACCESS_COARSE_LOCATION).isEmpty())

private val _mapCenter = MutableStateFlow<LatLng>(LatLng(0.0, 0.0))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ data class SuggestedService(
val website: String,
val termsOfUse: String,
val hostedBy: String,
val endpoint: Endpoint
val endpoint: Endpoint,
val coverage: Coverage
) {
companion object {
fun getSuggestedServices(context: Context): List<SuggestedService> {
Expand All @@ -31,4 +32,10 @@ data class SuggestedService(
val path: String,
val apiKey: String? = null
)
}

@Serializable
data class Coverage(
val tileJson: String? = null,
val layerId: String? = null
)
}
4 changes: 4 additions & 0 deletions app/src/main/res/raw/suggested_services.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"endpoint": {
"baseUrl": "https://api.beacondb.net",
"path": "/v2/geosubmit"
},
"coverage": {
"tileJson": "https://cdn.beacondb.net/tiles/beacondb.json",
"layerId": "beacondb"
}
}
]
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@
<string name="path">Path</string>
<string name="api_key">API key (optional)</string>

<string name="tile_json">TileJSON for coverage map (optional)</string>
<string name="layer_id">Layer ID of TileJSON for coverage map (optional)</string>

<string name="unencrypted_endpoint_warning">Unencrypted endpoint</string>

<string name="suggested_services_title">Suggested services</string>
Expand Down

0 comments on commit e833de1

Please sign in to comment.