Implement legacy-style map markers with UI model mapping.
Replace default OSM pins with round value-based icons, add DTO-to-domain-to-UI marker mapping, and normalize no-value/offline styling while keeping ownership icon behavior stubbed for future auth integration. Made-with: Cursor
This commit is contained in:
15
app/src/main/kotlin/org/db3/airmq/features/map/MapMarker.kt
Normal file
15
app/src/main/kotlin/org/db3/airmq/features/map/MapMarker.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package org.db3.airmq.features.map
|
||||||
|
|
||||||
|
import org.db3.airmq.features.map.MapScreenContract.SensorType
|
||||||
|
|
||||||
|
data class MapMarker(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val city: String?,
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val isOnline: Boolean,
|
||||||
|
val sensorType: SensorType,
|
||||||
|
val value: Double?,
|
||||||
|
val isOwned: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.db3.airmq.features.map
|
||||||
|
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import org.db3.airmq.R
|
||||||
|
import org.db3.airmq.features.map.MapScreenContract.SensorType
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
object MapMarkerStyle {
|
||||||
|
|
||||||
|
fun formatValue(value: Double?, sensorType: SensorType): String {
|
||||||
|
if (value == null) return "--"
|
||||||
|
return when (sensorType) {
|
||||||
|
SensorType.DUST -> value.roundToInt().toString()
|
||||||
|
SensorType.RADIOACTIVITY -> formatAdaptive(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorRes
|
||||||
|
fun valueColorRes(value: Double?, sensorType: SensorType): Int {
|
||||||
|
if (value == null) return R.color.colorGrey
|
||||||
|
return when (sensorType) {
|
||||||
|
SensorType.DUST -> when {
|
||||||
|
value <= 12.0 -> R.color.sensorGreen
|
||||||
|
value <= 35.4 -> R.color.sensorYellow
|
||||||
|
value <= 55.4 -> R.color.sensorOrange
|
||||||
|
value <= 150.4 -> R.color.sensorRed
|
||||||
|
value <= 250.4 -> R.color.sensorPink
|
||||||
|
else -> R.color.sensorPurple
|
||||||
|
}
|
||||||
|
SensorType.RADIOACTIVITY -> R.color.sensorGreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatAdaptive(value: Double): String {
|
||||||
|
val decimals = when {
|
||||||
|
value > 10.0 -> 0
|
||||||
|
value > 1.0 -> 1
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
return if (decimals == 0) {
|
||||||
|
value.roundToInt().toString()
|
||||||
|
} else {
|
||||||
|
String.format(Locale.US, "%.${decimals}f", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
package org.db3.airmq.features.map
|
package org.db3.airmq.features.map
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -16,8 +22,10 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
@@ -25,14 +33,23 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import org.db3.airmq.R
|
import org.db3.airmq.R
|
||||||
import org.db3.airmq.features.map.MapScreenContract.Action
|
import org.db3.airmq.features.map.MapScreenContract.Action
|
||||||
|
import org.db3.airmq.features.map.MapScreenContract.DevicePanelState
|
||||||
|
import org.db3.airmq.features.map.MapScreenContract.DeviceSensorType
|
||||||
import org.db3.airmq.features.map.MapScreenContract.Event
|
import org.db3.airmq.features.map.MapScreenContract.Event
|
||||||
import org.db3.airmq.sdk.map.domain.MapItem
|
import org.db3.airmq.features.map.MapScreenContract.SearchPanelState
|
||||||
|
import org.db3.airmq.features.map.MapScreenContract.SearchResult
|
||||||
|
import org.db3.airmq.features.map.MapScreenContract.SensorType
|
||||||
|
import org.db3.airmq.features.map.MapScreenContract.State
|
||||||
|
import org.db3.airmq.features.map.MapScreenContract.TimeRange
|
||||||
|
import org.db3.airmq.ui.theme.AirMQTheme
|
||||||
import org.osmdroid.config.Configuration
|
import org.osmdroid.config.Configuration
|
||||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import org.osmdroid.views.MapView
|
import org.osmdroid.views.MapView
|
||||||
import org.osmdroid.views.overlay.Marker
|
import org.osmdroid.views.overlay.Marker
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MapScreen(
|
fun MapScreen(
|
||||||
@@ -58,15 +75,38 @@ fun MapScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MapScreenContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onEvent = viewModel::onEvent,
|
||||||
|
showMap = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MapScreenContent(
|
||||||
|
uiState: State,
|
||||||
|
onEvent: (Event) -> Unit,
|
||||||
|
showMap: Boolean
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
AirMQMap(
|
if (showMap) {
|
||||||
items = uiState.items,
|
AirMQMap(
|
||||||
onMarkerClick = { viewModel.onEvent(Event.MarkerClicked(it)) }
|
items = uiState.items,
|
||||||
)
|
onMarkerClick = { onEvent(Event.MarkerClicked(it)) }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFE8F1E5))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
MapTopControls(
|
MapTopControls(
|
||||||
selectedSensor = uiState.selectedTopSensor,
|
selectedSensor = uiState.selectedTopSensor,
|
||||||
onSensorSelected = { viewModel.onEvent(Event.TopSensorSelected(it)) },
|
onSensorSelected = { onEvent(Event.TopSensorSelected(it)) },
|
||||||
onHelpClick = {
|
onHelpClick = {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
@@ -81,8 +121,8 @@ fun MapScreen(
|
|||||||
|
|
||||||
if (uiState.searchPanelState == null && uiState.devicePanelState == null) {
|
if (uiState.searchPanelState == null && uiState.devicePanelState == null) {
|
||||||
MapFloatingActions(
|
MapFloatingActions(
|
||||||
onSearchClick = { viewModel.onEvent(Event.SearchButtonClicked) },
|
onSearchClick = { onEvent(Event.SearchButtonClicked) },
|
||||||
onMyLocationClick = { viewModel.onEvent(Event.MyLocationClicked) },
|
onMyLocationClick = { onEvent(Event.MyLocationClicked) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(bottom = 92.dp, end = 16.dp)
|
.padding(bottom = 92.dp, end = 16.dp)
|
||||||
@@ -93,9 +133,9 @@ fun MapScreen(
|
|||||||
MapSearchOverlay(
|
MapSearchOverlay(
|
||||||
query = searchPanelState.query,
|
query = searchPanelState.query,
|
||||||
results = searchPanelState.results,
|
results = searchPanelState.results,
|
||||||
onQueryChanged = { viewModel.onEvent(Event.SearchQueryChanged(it)) },
|
onQueryChanged = { onEvent(Event.SearchQueryChanged(it)) },
|
||||||
onClose = { viewModel.onEvent(Event.SearchClosed) },
|
onClose = { onEvent(Event.SearchClosed) },
|
||||||
onResultClick = { viewModel.onEvent(Event.SearchResultClicked(it.id)) },
|
onResultClick = { onEvent(Event.SearchResultClicked(it.id)) },
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -103,12 +143,12 @@ fun MapScreen(
|
|||||||
uiState.devicePanelState?.let { panelData ->
|
uiState.devicePanelState?.let { panelData ->
|
||||||
MapDevicePanel(
|
MapDevicePanel(
|
||||||
data = panelData,
|
data = panelData,
|
||||||
onClose = { viewModel.onEvent(Event.DevicePanelClosed) },
|
onClose = { onEvent(Event.DevicePanelClosed) },
|
||||||
onOpenDevice = { viewModel.onEvent(Event.DeviceOpenClicked) },
|
onOpenDevice = { onEvent(Event.DeviceOpenClicked) },
|
||||||
onRangeSelected = { viewModel.onEvent(Event.TimeRangeSelected(it)) },
|
onRangeSelected = { onEvent(Event.TimeRangeSelected(it)) },
|
||||||
onDateBack = { viewModel.onEvent(Event.DateBackClicked) },
|
onDateBack = { onEvent(Event.DateBackClicked) },
|
||||||
onDateForward = { viewModel.onEvent(Event.DateForwardClicked) },
|
onDateForward = { onEvent(Event.DateForwardClicked) },
|
||||||
onSensorSelected = { viewModel.onEvent(Event.DeviceSensorSelected(it)) },
|
onSensorSelected = { onEvent(Event.DeviceSensorSelected(it)) },
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -129,7 +169,7 @@ fun MapScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AirMQMap(
|
private fun AirMQMap(
|
||||||
items: List<MapItem>,
|
items: List<MapMarker>,
|
||||||
onMarkerClick: (String) -> Unit
|
onMarkerClick: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -171,6 +211,7 @@ private fun AirMQMap(
|
|||||||
title = listOfNotNull(item.title, item.city).joinToString(" - ")
|
title = listOfNotNull(item.title, item.city).joinToString(" - ")
|
||||||
subDescription = if (item.isOnline) "Online" else "Offline"
|
subDescription = if (item.isOnline) "Online" else "Offline"
|
||||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||||
|
icon = createMarkerIcon(map, item)
|
||||||
setOnMarkerClickListener { _, _ ->
|
setOnMarkerClickListener { _, _ ->
|
||||||
onMarkerClick(item.id)
|
onMarkerClick(item.id)
|
||||||
true
|
true
|
||||||
@@ -187,3 +228,122 @@ private fun AirMQMap(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createMarkerIcon(mapView: MapView, item: MapMarker): BitmapDrawable {
|
||||||
|
val context = mapView.context
|
||||||
|
val iconView = LayoutInflater.from(context).inflate(R.layout.view_map_marker, null, false)
|
||||||
|
val markerBorder = iconView.findViewById<android.view.View>(R.id.marker_border)
|
||||||
|
val markerCenter = iconView.findViewById<android.view.View>(R.id.marker_center)
|
||||||
|
val markerText = iconView.findViewById<TextView>(R.id.marker_text)
|
||||||
|
val markerShade = iconView.findViewById<android.view.View>(R.id.marker_image_shade)
|
||||||
|
val markerImage = iconView.findViewById<ImageView>(R.id.marker_image)
|
||||||
|
|
||||||
|
val hasValue = item.value != null
|
||||||
|
val isNoValueStyled = !hasValue
|
||||||
|
val colorRes = if (isNoValueStyled) {
|
||||||
|
R.color.colorGrey
|
||||||
|
} else {
|
||||||
|
MapMarkerStyle.valueColorRes(item.value, item.sensorType)
|
||||||
|
}
|
||||||
|
val markerColor = ContextCompat.getColor(context, colorRes)
|
||||||
|
val centerColor = if (isNoValueStyled) R.color.colorGrey else R.color.white
|
||||||
|
markerBorder.backgroundTintList = android.content.res.ColorStateList.valueOf(markerColor)
|
||||||
|
markerCenter.backgroundTintList =
|
||||||
|
android.content.res.ColorStateList.valueOf(ContextCompat.getColor(context, centerColor))
|
||||||
|
|
||||||
|
markerText.text = MapMarkerStyle.formatValue(item.value, item.sensorType)
|
||||||
|
if (item.isOwned && !isNoValueStyled) {
|
||||||
|
markerImage.setImageResource(R.drawable.ic_marker_user)
|
||||||
|
markerImage.visibility = android.view.View.VISIBLE
|
||||||
|
markerShade.visibility = android.view.View.VISIBLE
|
||||||
|
markerText.setTextColor(ContextCompat.getColor(context, R.color.white))
|
||||||
|
} else {
|
||||||
|
markerImage.visibility = android.view.View.GONE
|
||||||
|
markerShade.visibility = android.view.View.GONE
|
||||||
|
markerText.setTextColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
context,
|
||||||
|
if (isNoValueStyled) R.color.white else colorRes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
iconView.measure(
|
||||||
|
android.view.View.MeasureSpec.makeMeasureSpec(0, android.view.View.MeasureSpec.UNSPECIFIED),
|
||||||
|
android.view.View.MeasureSpec.makeMeasureSpec(0, android.view.View.MeasureSpec.UNSPECIFIED)
|
||||||
|
)
|
||||||
|
iconView.layout(0, 0, iconView.measuredWidth, iconView.measuredHeight)
|
||||||
|
val bitmap = createBitmap(iconView.measuredWidth, iconView.measuredHeight)
|
||||||
|
iconView.draw(Canvas(bitmap))
|
||||||
|
return bitmap.toDrawable(context.resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMapScreenDefault() {
|
||||||
|
AirMQTheme {
|
||||||
|
MapScreenContent(
|
||||||
|
uiState = State(
|
||||||
|
selectedTopSensor = SensorType.DUST,
|
||||||
|
items = listOf(
|
||||||
|
MapMarker(
|
||||||
|
id = "1",
|
||||||
|
title = "AirMQ #1",
|
||||||
|
city = "Minsk",
|
||||||
|
latitude = 53.9,
|
||||||
|
longitude = 27.56,
|
||||||
|
isOnline = true,
|
||||||
|
sensorType = SensorType.DUST,
|
||||||
|
value = 12.0,
|
||||||
|
isOwned = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onEvent = {},
|
||||||
|
showMap = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMapScreenSearch() {
|
||||||
|
AirMQTheme {
|
||||||
|
MapScreenContent(
|
||||||
|
uiState = State(
|
||||||
|
selectedTopSensor = SensorType.RADIOACTIVITY,
|
||||||
|
searchPanelState = SearchPanelState(
|
||||||
|
query = "Minsk",
|
||||||
|
results = listOf(
|
||||||
|
SearchResult(id = "1", title = "AirMQ #42", subtitle = "Minsk"),
|
||||||
|
SearchResult(id = "2", title = "AirMQ #91", subtitle = "Salihorsk")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onEvent = {},
|
||||||
|
showMap = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMapScreenDevicePanel() {
|
||||||
|
AirMQTheme {
|
||||||
|
MapScreenContent(
|
||||||
|
uiState = State(
|
||||||
|
selectedTopSensor = SensorType.DUST,
|
||||||
|
devicePanelState = DevicePanelState(
|
||||||
|
id = "42",
|
||||||
|
name = "AirMQ #42",
|
||||||
|
status = "Online",
|
||||||
|
selectedRange = TimeRange.DAY,
|
||||||
|
displayedDateRange = "Today",
|
||||||
|
selectedSensor = DeviceSensorType.DUST
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onEvent = {},
|
||||||
|
showMap = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.db3.airmq.features.map
|
package org.db3.airmq.features.map
|
||||||
|
|
||||||
import org.db3.airmq.sdk.map.domain.MapItem
|
|
||||||
|
|
||||||
object MapScreenContract {
|
object MapScreenContract {
|
||||||
|
|
||||||
enum class SensorType {
|
enum class SensorType {
|
||||||
@@ -44,7 +42,7 @@ object MapScreenContract {
|
|||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val items: List<MapItem> = emptyList(),
|
val items: List<MapMarker> = emptyList(),
|
||||||
val selectedTopSensor: SensorType = SensorType.DUST,
|
val selectedTopSensor: SensorType = SensorType.DUST,
|
||||||
val searchPanelState: SearchPanelState? = null,
|
val searchPanelState: SearchPanelState? = null,
|
||||||
val devicePanelState: DevicePanelState? = null
|
val devicePanelState: DevicePanelState? = null
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.db3.airmq.features.map.MapScreenContract.SensorType
|
|||||||
import org.db3.airmq.features.map.MapScreenContract.State
|
import org.db3.airmq.features.map.MapScreenContract.State
|
||||||
import org.db3.airmq.features.map.MapScreenContract.TimeRange
|
import org.db3.airmq.features.map.MapScreenContract.TimeRange
|
||||||
import org.db3.airmq.sdk.map.MapService
|
import org.db3.airmq.sdk.map.MapService
|
||||||
|
import org.db3.airmq.sdk.map.domain.MapItem
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MapViewModel @Inject constructor(
|
class MapViewModel @Inject constructor(
|
||||||
@@ -30,6 +31,7 @@ class MapViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val _uiState = MutableStateFlow(State(isLoading = true))
|
private val _uiState = MutableStateFlow(State(isLoading = true))
|
||||||
val uiState: StateFlow<State> = _uiState.asStateFlow()
|
val uiState: StateFlow<State> = _uiState.asStateFlow()
|
||||||
|
private var domainItems: List<MapItem> = emptyList()
|
||||||
|
|
||||||
private val _actions = MutableSharedFlow<Action>(extraBufferCapacity = 1)
|
private val _actions = MutableSharedFlow<Action>(extraBufferCapacity = 1)
|
||||||
val actions: SharedFlow<Action> = _actions.asSharedFlow()
|
val actions: SharedFlow<Action> = _actions.asSharedFlow()
|
||||||
@@ -82,6 +84,7 @@ class MapViewModel @Inject constructor(
|
|||||||
|
|
||||||
is Event.TopSensorSelected -> {
|
is Event.TopSensorSelected -> {
|
||||||
_uiState.value = _uiState.value.copy(selectedTopSensor = event.sensor)
|
_uiState.value = _uiState.value.copy(selectedTopSensor = event.sensor)
|
||||||
|
remapMarkers()
|
||||||
}
|
}
|
||||||
|
|
||||||
is Event.MarkerClicked -> {
|
is Event.MarkerClicked -> {
|
||||||
@@ -141,16 +144,19 @@ class MapViewModel @Inject constructor(
|
|||||||
val result = runCatching { mapService.fetchMapItems() }
|
val result = runCatching { mapService.fetchMapItems() }
|
||||||
_uiState.value = result.fold(
|
_uiState.value = result.fold(
|
||||||
onSuccess = { items ->
|
onSuccess = { items ->
|
||||||
|
domainItems = items
|
||||||
val searchPanelState = _uiState.value.searchPanelState
|
val searchPanelState = _uiState.value.searchPanelState
|
||||||
|
val markers = items.toMarkers(_uiState.value.selectedTopSensor)
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
items = items,
|
items = markers,
|
||||||
searchPanelState = searchPanelState?.copy(
|
searchPanelState = searchPanelState?.copy(
|
||||||
results = resolveSearchResults(searchPanelState.query)
|
results = resolveSearchResults(searchPanelState.query)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onFailure = { throwable ->
|
onFailure = { throwable ->
|
||||||
|
domainItems = emptyList()
|
||||||
_actions.tryEmit(Action.ShowToast(throwable.message ?: "Failed to load map items"))
|
_actions.tryEmit(Action.ShowToast(throwable.message ?: "Failed to load map items"))
|
||||||
val searchPanelState = _uiState.value.searchPanelState
|
val searchPanelState = _uiState.value.searchPanelState
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
@@ -187,11 +193,35 @@ class MapViewModel @Inject constructor(
|
|||||||
TimeRange.MONTH -> "This month"
|
TimeRange.MONTH -> "This month"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun org.db3.airmq.sdk.map.domain.MapItem.toDevicePanelState(): DevicePanelState {
|
private fun remapMarkers() {
|
||||||
val defaultSensor = when {
|
_uiState.value = _uiState.value.copy(
|
||||||
title.contains("radiation", ignoreCase = true) -> DeviceSensorType.RADIOACTIVITY
|
items = domainItems.toMarkers(_uiState.value.selectedTopSensor)
|
||||||
title.contains("dust", ignoreCase = true) -> DeviceSensorType.DUST
|
)
|
||||||
else -> DeviceSensorType.TEMPERATURE
|
}
|
||||||
|
|
||||||
|
private fun List<MapItem>.toMarkers(sensorType: SensorType): List<MapMarker> {
|
||||||
|
return map { item ->
|
||||||
|
MapMarker(
|
||||||
|
id = item.id,
|
||||||
|
title = item.title,
|
||||||
|
city = item.city,
|
||||||
|
latitude = item.latitude,
|
||||||
|
longitude = item.longitude,
|
||||||
|
isOnline = item.isOnline,
|
||||||
|
sensorType = sensorType,
|
||||||
|
value = when (sensorType) {
|
||||||
|
SensorType.DUST -> item.dustValue
|
||||||
|
SensorType.RADIOACTIVITY -> item.radioactivityValue
|
||||||
|
},
|
||||||
|
isOwned = false // TODO: derive from authenticated user when ownership/auth is implemented.
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MapMarker.toDevicePanelState(): DevicePanelState {
|
||||||
|
val defaultSensor = when (sensorType) {
|
||||||
|
SensorType.DUST -> DeviceSensorType.DUST
|
||||||
|
SensorType.RADIOACTIVITY -> DeviceSensorType.RADIOACTIVITY
|
||||||
}
|
}
|
||||||
return DevicePanelState(
|
return DevicePanelState(
|
||||||
id = id,
|
id = id,
|
||||||
|
|||||||
4
app/src/main/res/drawable/circle_marker.xml
Normal file
4
app/src/main/res/drawable/circle_marker.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||||
|
<solid android:color="@color/white" />
|
||||||
|
</shape>
|
||||||
4
app/src/main/res/drawable/circle_marker_shade.xml
Normal file
4
app/src/main/res/drawable/circle_marker_shade.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||||
|
<solid android:color="#66000000" />
|
||||||
|
</shape>
|
||||||
10
app/src/main/res/drawable/ic_marker_user.xml
Normal file
10
app/src/main/res/drawable/ic_marker_user.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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="#FFFFFFFF"
|
||||||
|
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
|
||||||
|
</vector>
|
||||||
50
app/src/main/res/layout/view_map_marker.xml
Normal file
50
app/src/main/res/layout/view_map_marker.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:padding="4dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/circle_marker" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/marker_border"
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/circle_marker" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/marker_center"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/circle_marker" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/marker_image"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/marker_image_shade"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/circle_marker_shade"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/marker_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -7,4 +7,11 @@
|
|||||||
<color name="teal_700">#FF018786</color>
|
<color name="teal_700">#FF018786</color>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="colorGrey">#FF9E9E9E</color>
|
||||||
|
<color name="sensorGreen">#FF00C853</color>
|
||||||
|
<color name="sensorYellow">#FFFFD54F</color>
|
||||||
|
<color name="sensorOrange">#FFFF9800</color>
|
||||||
|
<color name="sensorRed">#FFF44336</color>
|
||||||
|
<color name="sensorPink">#FFEC407A</color>
|
||||||
|
<color name="sensorPurple">#FF8E24AA</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
query MapMarkers($isOnline: Boolean!) {
|
query MapMarkers {
|
||||||
locations(filter: { isOnline: $isOnline }) {
|
locations {
|
||||||
_id
|
_id
|
||||||
name
|
name
|
||||||
city
|
city
|
||||||
@@ -9,6 +9,7 @@ query MapMarkers($isOnline: Boolean!) {
|
|||||||
metricList
|
metricList
|
||||||
currentValue {
|
currentValue {
|
||||||
PMS25
|
PMS25
|
||||||
|
Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,9 @@ class ApolloMapItemMapper @Inject constructor() {
|
|||||||
city = location.city?.ifBlank { null },
|
city = location.city?.ifBlank { null },
|
||||||
latitude = location.latitude,
|
latitude = location.latitude,
|
||||||
longitude = location.longitude,
|
longitude = location.longitude,
|
||||||
isOnline = location.isOnline ?: false
|
isOnline = location.isOnline ?: false,
|
||||||
|
dustValue = location.currentValue?.PMS25?.toDouble(),
|
||||||
|
radioactivityValue = location.currentValue?.Count?.toDouble()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class MapServiceImpl @Inject constructor(
|
|||||||
override suspend fun fetchMapItems(): List<MapItem> {
|
override suspend fun fetchMapItems(): List<MapItem> {
|
||||||
Log.d(TAG, "Executing MapMarkers Apollo query")
|
Log.d(TAG, "Executing MapMarkers Apollo query")
|
||||||
val response = apolloClient
|
val response = apolloClient
|
||||||
.query(MapMarkersQuery(isOnline = false))
|
.query(MapMarkersQuery())
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
response.errors?.firstOrNull()?.let { gqlError ->
|
response.errors?.firstOrNull()?.let { gqlError ->
|
||||||
|
|||||||
@@ -6,5 +6,7 @@ data class MapItem(
|
|||||||
val city: String?,
|
val city: String?,
|
||||||
val latitude: Double,
|
val latitude: Double,
|
||||||
val longitude: Double,
|
val longitude: Double,
|
||||||
val isOnline: Boolean
|
val isOnline: Boolean,
|
||||||
|
val dustValue: Double?,
|
||||||
|
val radioactivityValue: Double?
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user