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:
@@ -30,13 +30,6 @@ android {
|
|||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
// Inject Mapbox public token as a BuildConfig field
|
|
||||||
buildConfigField(
|
|
||||||
"String",
|
|
||||||
"MAPBOX_PUBLIC_TOKEN",
|
|
||||||
"\"${localProperties.getProperty("MAPBOX_PUBLIC_TOKEN", "")}\""
|
|
||||||
)
|
|
||||||
|
|
||||||
// Backend base URL
|
// Backend base URL
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
@@ -121,8 +114,8 @@ dependencies {
|
|||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.retrofit.kotlinx.serialization)
|
implementation(libs.retrofit.kotlinx.serialization)
|
||||||
|
|
||||||
// Mapbox (uncomment after setting MAPBOX_DOWNLOADS_TOKEN in local.properties)
|
// MapLibre (free, no API key needed)
|
||||||
// implementation(libs.mapbox.maps)
|
implementation(libs.maplibre.compose)
|
||||||
|
|
||||||
// Firebase (uncomment when google-services.json is added)
|
// Firebase (uncomment when google-services.json is added)
|
||||||
// implementation(platform(libs.firebase.bom))
|
// implementation(platform(libs.firebase.bom))
|
||||||
|
|||||||
@@ -2,25 +2,147 @@ package com.geozoner.app.ui.map
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.SmallFloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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
|
@Composable
|
||||||
fun MapScreen() {
|
fun MapScreen(
|
||||||
Box(
|
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(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center,
|
baseStyle = org.maplibre.compose.style.BaseStyle.Uri(OPENFREEMAP_STYLE),
|
||||||
) {
|
) {
|
||||||
Text(
|
// Friend zones layer (rendered below own zones)
|
||||||
text = "Map Screen\n(Mapbox integration pending)",
|
if (friendZoneGeoJson.isNotEmpty()) {
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,8 +27,8 @@ okhttp = "4.12.0"
|
|||||||
kotlinxSerialization = "1.7.3"
|
kotlinxSerialization = "1.7.3"
|
||||||
retrofitKotlinxSerialization = "1.0.0"
|
retrofitKotlinxSerialization = "1.0.0"
|
||||||
|
|
||||||
# Mapbox
|
# MapLibre
|
||||||
mapbox = "11.8.2"
|
maplibreCompose = "0.12.1"
|
||||||
|
|
||||||
# Firebase
|
# Firebase
|
||||||
firebaseBom = "33.7.0"
|
firebaseBom = "33.7.0"
|
||||||
@@ -85,8 +85,8 @@ okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor",
|
|||||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||||
retrofit-kotlinx-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerialization" }
|
retrofit-kotlinx-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerialization" }
|
||||||
|
|
||||||
# Mapbox
|
# MapLibre
|
||||||
mapbox-maps = { group = "com.mapbox.maps", name = "android", version.ref = "mapbox" }
|
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibreCompose" }
|
||||||
|
|
||||||
# Firebase
|
# Firebase
|
||||||
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
|
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
|
||||||
|
|||||||
@@ -17,16 +17,6 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
|
||||||
url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
|
|
||||||
credentials.username = "mapbox"
|
|
||||||
credentials.password = providers.gradleProperty("MAPBOX_DOWNLOADS_TOKEN")
|
|
||||||
.orElse(providers.environmentVariable("MAPBOX_DOWNLOADS_TOKEN"))
|
|
||||||
.getOrElse("")
|
|
||||||
authentication {
|
|
||||||
create<BasicAuthentication>("basic")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user