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:
Redsandy
2026-03-14 23:58:27 +03:00
parent 4cc43a410b
commit ffc5d0887e
5 changed files with 193 additions and 36 deletions

View File

@@ -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))

View File

@@ -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(",")}]}"""
} }

View 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,
)
}
}
}
}

View File

@@ -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" }

View File

@@ -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")
}
}
} }
} }