Refactor map integration to use MapLibre instead of Mapbox
- Removed Mapbox dependencies and related configurations from settings.gradle.kts and build.gradle.kts. - Implemented MapViewModel to manage zone data and loading state. - Updated MapScreen to render zones using MapLibre, including loading indicators and error handling. - Added GeoJSON generation for zone data to be displayed on the map. - Updated libraries version to use MapLibre Compose.
This commit is contained in:
@@ -2,25 +2,147 @@ package com.geozoner.app.ui.map
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.geozoner.app.data.remote.dto.ZoneBriefResponse
|
||||
import com.geozoner.app.ui.theme.ZoneColors
|
||||
import org.maplibre.compose.expressions.dsl.const
|
||||
import org.maplibre.compose.layers.FillLayer
|
||||
import org.maplibre.compose.layers.LineLayer
|
||||
import org.maplibre.compose.map.MaplibreMap
|
||||
import org.maplibre.compose.sources.GeoJsonData
|
||||
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||
|
||||
private const val OPENFREEMAP_STYLE = "https://tiles.openfreemap.org/styles/liberty"
|
||||
|
||||
/**
|
||||
* Map screen — will display Mapbox map with zone polygons.
|
||||
* TODO: Integrate Mapbox SDK, render own + friend zones, live GPS track.
|
||||
*/
|
||||
@Composable
|
||||
fun MapScreen() {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "Map Screen\n(Mapbox integration pending)",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
)
|
||||
fun MapScreen(
|
||||
viewModel: MapViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
// Build GeoJSON strings for own and friend zones
|
||||
val myZoneGeoJson = remember(state.myZones) {
|
||||
buildZoneGeoJson(state.myZones)
|
||||
}
|
||||
|
||||
val friendZoneGeoJson = remember(state.friendZones) {
|
||||
buildZoneGeoJson(state.friendZones)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
MaplibreMap(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
baseStyle = org.maplibre.compose.style.BaseStyle.Uri(OPENFREEMAP_STYLE),
|
||||
) {
|
||||
// Friend zones layer (rendered below own zones)
|
||||
if (friendZoneGeoJson.isNotEmpty()) {
|
||||
val friendSource = rememberGeoJsonSource(
|
||||
data = GeoJsonData.JsonString(friendZoneGeoJson),
|
||||
)
|
||||
|
||||
FillLayer(
|
||||
id = "friend-zones-fill",
|
||||
source = friendSource,
|
||||
color = const(Color(0xFF2196F3)),
|
||||
opacity = const(0.25f),
|
||||
)
|
||||
LineLayer(
|
||||
id = "friend-zones-outline",
|
||||
source = friendSource,
|
||||
color = const(Color(0xFF1976D2)),
|
||||
width = const(2.dp),
|
||||
opacity = const(0.7f),
|
||||
)
|
||||
}
|
||||
|
||||
// Own zones layer (on top)
|
||||
if (myZoneGeoJson.isNotEmpty()) {
|
||||
val mySource = rememberGeoJsonSource(
|
||||
data = GeoJsonData.JsonString(myZoneGeoJson),
|
||||
)
|
||||
|
||||
FillLayer(
|
||||
id = "my-zones-fill",
|
||||
source = mySource,
|
||||
color = const(Color(0xFF4CAF50)),
|
||||
opacity = const(0.35f),
|
||||
)
|
||||
LineLayer(
|
||||
id = "my-zones-outline",
|
||||
source = mySource,
|
||||
color = const(Color(0xFF2E7D32)),
|
||||
width = const(3.dp),
|
||||
opacity = const(0.8f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
// Error message
|
||||
state.error?.let { error ->
|
||||
Text(
|
||||
text = error,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
// Refresh button
|
||||
SmallFloatingActionButton(
|
||||
onClick = { viewModel.loadZones() },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 16.dp, end = 16.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh zones")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a GeoJSON FeatureCollection string from zone responses.
|
||||
* Each zone becomes a Feature with a Polygon geometry.
|
||||
*/
|
||||
private fun buildZoneGeoJson(zones: List<ZoneBriefResponse>): String {
|
||||
if (zones.isEmpty()) return ""
|
||||
|
||||
val features = zones.mapNotNull { zone ->
|
||||
try {
|
||||
val polygon = zone.polygonGeojson.toString()
|
||||
"""{"type":"Feature","geometry":$polygon,"properties":{"zone_id":"${zone.id}","owner_id":"${zone.ownerId}","area_m2":${zone.areaM2},"defense_level":${zone.defenseLevel}}}"""
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return """{"type":"FeatureCollection","features":[${features.joinToString(",")}]}"""
|
||||
}
|
||||
|
||||
52
app/src/main/java/com/geozoner/app/ui/map/MapViewModel.kt
Normal file
52
app/src/main/java/com/geozoner/app/ui/map/MapViewModel.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
package com.geozoner.app.ui.map
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geozoner.app.data.remote.api.ZoneApi
|
||||
import com.geozoner.app.data.remote.dto.ZoneBriefResponse
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
data class MapUiState(
|
||||
val myZones: List<ZoneBriefResponse> = emptyList(),
|
||||
val friendZones: List<ZoneBriefResponse> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class MapViewModel @Inject constructor(
|
||||
private val zoneApi: ZoneApi,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(MapUiState())
|
||||
val uiState: StateFlow<MapUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadZones()
|
||||
}
|
||||
|
||||
fun loadZones() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||
try {
|
||||
val myZones = zoneApi.getMyZones()
|
||||
val friendZones = zoneApi.getFriendZones()
|
||||
_uiState.value = MapUiState(
|
||||
myZones = myZones,
|
||||
friendZones = friendZones,
|
||||
isLoading = false,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
error = e.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user