feat(ui): switch from google maps to maplibre
Wayy more performant and flexible
This commit is contained in:
parent
2ad6e5d9e6
commit
b9fa8f77c7
3 changed files with 154 additions and 108 deletions
|
|
@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.kotlinSerialization)
|
||||||
alias(libs.plugins.androidApplication)
|
alias(libs.plugins.androidApplication)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
|
|
@ -39,9 +40,6 @@ kotlin {
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.play.services.location)
|
implementation(libs.play.services.location)
|
||||||
implementation(libs.play.services.maps)
|
|
||||||
implementation(libs.maps.compose)
|
|
||||||
implementation(libs.maps.compose.utils)
|
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(libs.compose.components.resources)
|
implementation(libs.compose.components.resources)
|
||||||
|
|
@ -62,6 +60,7 @@ kotlin {
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.contentnegotiation)
|
implementation(libs.ktor.client.contentnegotiation)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
|
implementation(libs.maplibre.compose)
|
||||||
implementation(libs.moko.geo)
|
implementation(libs.moko.geo)
|
||||||
implementation(libs.moko.geo.compose)
|
implementation(libs.moko.geo.compose)
|
||||||
implementation(projects.shared)
|
implementation(projects.shared)
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,20 @@
|
||||||
|
@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
|
||||||
|
|
||||||
package moe.lava.banksia.ui.platform.maps
|
package moe.lava.banksia.ui.platform.maps
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.add
|
import androidx.compose.foundation.layout.add
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
|
@ -27,28 +22,43 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
import com.google.android.gms.maps.CameraUpdateFactory
|
|
||||||
import com.google.android.gms.maps.model.LatLng
|
|
||||||
import com.google.android.gms.maps.model.LatLngBounds
|
|
||||||
import com.google.android.gms.maps.model.MapStyleOptions
|
|
||||||
import com.google.maps.android.compose.ComposeMapColorScheme
|
|
||||||
import com.google.maps.android.compose.DefaultMapProperties
|
|
||||||
import com.google.maps.android.compose.DefaultMapUiSettings
|
|
||||||
import com.google.maps.android.compose.GoogleMap
|
|
||||||
import com.google.maps.android.compose.MarkerComposable
|
|
||||||
import com.google.maps.android.compose.Polyline
|
|
||||||
import com.google.maps.android.compose.rememberCameraPositionState
|
|
||||||
import com.google.maps.android.compose.rememberUpdatedMarkerState
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import moe.lava.banksia.R
|
import kotlinx.serialization.Serializable
|
||||||
import moe.lava.banksia.ui.components.RouteIcon
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import moe.lava.banksia.model.RouteType
|
||||||
|
import moe.lava.banksia.ui.components.getUIProperties
|
||||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||||
|
import moe.lava.banksia.ui.screens.MELBOURNE
|
||||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||||
import moe.lava.banksia.ui.state.MapState
|
import moe.lava.banksia.ui.state.MapState
|
||||||
import moe.lava.banksia.util.BoxedValue
|
import moe.lava.banksia.util.BoxedValue
|
||||||
import moe.lava.banksia.util.Point
|
import moe.lava.banksia.util.Point
|
||||||
|
import moe.lava.banksia.util.log
|
||||||
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
|
import org.maplibre.compose.expressions.dsl.case
|
||||||
|
import org.maplibre.compose.expressions.dsl.const
|
||||||
|
import org.maplibre.compose.expressions.dsl.convertToString
|
||||||
|
import org.maplibre.compose.expressions.dsl.feature
|
||||||
|
import org.maplibre.compose.expressions.dsl.switch
|
||||||
|
import org.maplibre.compose.layers.CircleLayer
|
||||||
|
import org.maplibre.compose.map.MapOptions
|
||||||
|
import org.maplibre.compose.map.MaplibreMap
|
||||||
|
import org.maplibre.compose.map.OrnamentOptions
|
||||||
|
import org.maplibre.compose.sources.GeoJsonData
|
||||||
|
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||||
|
import org.maplibre.compose.style.BaseStyle
|
||||||
|
import org.maplibre.compose.util.ClickResult
|
||||||
|
import org.maplibre.spatialk.geojson.BoundingBox
|
||||||
|
import org.maplibre.spatialk.geojson.FeatureCollection
|
||||||
|
import org.maplibre.spatialk.geojson.Position
|
||||||
|
import org.maplibre.spatialk.geojson.dsl.addFeature
|
||||||
|
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
|
||||||
|
|
||||||
fun Point.toLatLng(): LatLng = LatLng(this.lat, this.lng)
|
import org.maplibre.compose.camera.CameraPosition as MLCameraPosition
|
||||||
|
import org.maplibre.spatialk.geojson.Point as MLPoint
|
||||||
|
|
||||||
|
fun Point.toPos(): Position = Position(this.lng, this.lat)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun checkLocationPermission() =
|
private fun checkLocationPermission() =
|
||||||
|
|
@ -62,6 +72,40 @@ actual fun getScreenHeight(): Int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MarkerProps(
|
||||||
|
val type: RouteType,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun buildMarkers(markers: List<Marker>): FeatureCollection<MLPoint, MarkerProps> {
|
||||||
|
return buildFeatureCollection {
|
||||||
|
markers.forEach { marker ->
|
||||||
|
val type = when (marker) {
|
||||||
|
is Marker.Stop -> marker.type
|
||||||
|
is Marker.Vehicle -> marker.type
|
||||||
|
}
|
||||||
|
val id = when (marker) {
|
||||||
|
is Marker.Stop -> marker.id
|
||||||
|
is Marker.Vehicle -> marker.ref
|
||||||
|
}
|
||||||
|
addFeature(
|
||||||
|
geometry = MLPoint(marker.point.toPos()),
|
||||||
|
properties = MarkerProps(type),
|
||||||
|
) {
|
||||||
|
setId(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val colorTypeExpression @Composable get() = switch(
|
||||||
|
input = feature["type"].convertToString(),
|
||||||
|
cases = RouteType.entries.map {
|
||||||
|
case(label = it.name, output = const(it.getUIProperties().colour))
|
||||||
|
}.toTypedArray(),
|
||||||
|
fallback = const(BanksiaTheme.colors.surface),
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
actual fun Maps(
|
actual fun Maps(
|
||||||
|
|
@ -72,104 +116,110 @@ actual fun Maps(
|
||||||
setLastKnownLocation: (Point) -> Unit,
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
extInsets: WindowInsets,
|
extInsets: WindowInsets,
|
||||||
) {
|
) {
|
||||||
val camPos = rememberCameraPositionState()
|
val camPos = rememberCameraState(
|
||||||
|
MLCameraPosition(
|
||||||
|
zoom = 16.0,
|
||||||
|
target = MELBOURNE.toPos()
|
||||||
|
)
|
||||||
|
)
|
||||||
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
|
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
|
||||||
LaunchedEffect(newCameraPos) {
|
LaunchedEffect(newCameraPos) {
|
||||||
|
log("maps", "newPos ${newCameraPos?.value}")
|
||||||
val pos = newCameraPos?.value ?: return@LaunchedEffect
|
val pos = newCameraPos?.value ?: return@LaunchedEffect
|
||||||
val update = if (pos.bounds != null) {
|
if (pos.bounds != null) {
|
||||||
val (northeast, southwest) = pos.bounds
|
val (northeast, southwest) = pos.bounds
|
||||||
val bounds = LatLngBounds(
|
camPos.animateTo(
|
||||||
southwest.toLatLng(),
|
boundingBox = BoundingBox(
|
||||||
northeast.toLatLng()
|
southwest.toPos(),
|
||||||
|
northeast.toPos()
|
||||||
)
|
)
|
||||||
CameraUpdateFactory.newLatLngBounds(bounds, 150)
|
)
|
||||||
} else
|
} else {
|
||||||
CameraUpdateFactory.newLatLngZoom(pos.centre.toLatLng(), 16.0f)
|
camPos.animateTo(MLCameraPosition(
|
||||||
|
target = pos.centre.toPos(),
|
||||||
camPos.animate(update, 1000)
|
zoom = 16.0,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ctx = LocalContext.current
|
val ctx = LocalContext.current
|
||||||
val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) }
|
val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
fusedLocation.lastLocation.addOnSuccessListener {
|
fusedLocation.lastLocation.addOnSuccessListener {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
camPos.position = com.google.android.gms.maps.model.CameraPosition(
|
camPos.position = MLCameraPosition(
|
||||||
LatLng(
|
zoom = 16.0,
|
||||||
it.latitude,
|
target = Position(it.longitude, it.latitude)
|
||||||
it.longitude
|
|
||||||
), 16.0f, 0.0f, 0.0f
|
|
||||||
)
|
)
|
||||||
setLastKnownLocation(Point(it.latitude, it.longitude))
|
setLastKnownLocation(Point(it.latitude, it.longitude))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GoogleMap(
|
MaplibreMap(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = modifier,
|
||||||
cameraPositionState = camPos,
|
baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/positron"),
|
||||||
mapColorScheme = if (isSystemInDarkTheme()) {
|
cameraState = camPos,
|
||||||
ComposeMapColorScheme.DARK
|
options = MapOptions(
|
||||||
} else {
|
ornamentOptions = OrnamentOptions(
|
||||||
ComposeMapColorScheme.LIGHT
|
padding = WindowInsets.safeDrawing.add(extInsets).asPaddingValues(),
|
||||||
},
|
isScaleBarEnabled = false,
|
||||||
properties = DefaultMapProperties.copy(
|
isAttributionEnabled = false,
|
||||||
mapStyleOptions = MapStyleOptions.loadRawResourceStyle(
|
)
|
||||||
LocalContext.current,
|
)
|
||||||
R.raw.def_mapstyle
|
|
||||||
),
|
|
||||||
isMyLocationEnabled = checkLocationPermission(),
|
|
||||||
),
|
|
||||||
uiSettings = DefaultMapUiSettings.copy(
|
|
||||||
zoomControlsEnabled = false,
|
|
||||||
myLocationButtonEnabled = false,
|
|
||||||
mapToolbarEnabled = false,
|
|
||||||
),
|
|
||||||
contentPadding = WindowInsets.safeDrawing.add(extInsets).asPaddingValues()
|
|
||||||
) {
|
) {
|
||||||
// [TODO]: Slight lag when routes with many stops such as the 901 bus is set
|
if (state.stops.isNotEmpty()) {
|
||||||
for (marker in state.stops) {
|
val stopsSource = rememberGeoJsonSource(
|
||||||
val state = rememberUpdatedMarkerState(marker.point.toLatLng())
|
GeoJsonData.Features(buildMarkers(state.stops))
|
||||||
MarkerComposable(
|
)
|
||||||
keys = arrayOf(marker),
|
CircleLayer(
|
||||||
zIndex = 0f,
|
id = "maps-stops0",
|
||||||
state = state,
|
source = stopsSource,
|
||||||
onClick = {
|
color = const(BanksiaTheme.colors.surface),
|
||||||
onEvent(MapScreenEvent.SelectStop(marker.type to marker.id))
|
radius = const(3.dp),
|
||||||
false
|
strokeWidth = const(2.dp),
|
||||||
|
strokeColor = colorTypeExpression,
|
||||||
|
onClick = { features ->
|
||||||
|
val feature = features[0]
|
||||||
|
val marker = Json.decodeFromJsonElement<MarkerProps>(feature.properties!!)
|
||||||
|
onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content))
|
||||||
|
ClickResult.Consume
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(12.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(BanksiaTheme.colors.surface)
|
|
||||||
.border(2.dp, marker.colour, CircleShape)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (marker in state.vehicles) {
|
|
||||||
val state = rememberUpdatedMarkerState(marker.point.toLatLng())
|
|
||||||
MarkerComposable(
|
|
||||||
keys = arrayOf(marker),
|
|
||||||
zIndex = 1f,
|
|
||||||
state = state,
|
|
||||||
onClick = {
|
|
||||||
onEvent(MapScreenEvent.SelectRun(marker.ref))
|
|
||||||
false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
RouteIcon(
|
|
||||||
size = 30.dp,
|
|
||||||
routeType = marker.type,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (polyline in state.polylines) {
|
|
||||||
Polyline(
|
|
||||||
points = polyline.points.map { it.toLatLng() },
|
|
||||||
color = polyline.colour
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// if (state.vehicles.isNotEmpty()) {
|
||||||
|
// val stopsSource = rememberGeoJsonSource(
|
||||||
|
// GeoJsonData.Features(buildMarkers(state.vehicles))
|
||||||
|
// )
|
||||||
|
// SymbolLayer
|
||||||
|
// CircleLayer(
|
||||||
|
// id = "maps-vehicles0",
|
||||||
|
// source = stopsSource,
|
||||||
|
// color = const(BanksiaTheme.colors.surface),
|
||||||
|
// radius = const(3.dp),
|
||||||
|
// strokeWidth = const(2.dp),
|
||||||
|
// strokeColor = colorTypeExpression,
|
||||||
|
// onClick = { features ->
|
||||||
|
// val feature = features[0]
|
||||||
|
// val marker = Json.decodeFromJsonElement<MarkerProps>(feature.properties!!)
|
||||||
|
// onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content))
|
||||||
|
// ClickResult.Consume
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (state.polylines.isNotEmpty()) {
|
||||||
|
// val polySource = rememberGeoJsonSource(
|
||||||
|
//
|
||||||
|
// )
|
||||||
|
// LineLayer(
|
||||||
|
// id = "maps-routeline",
|
||||||
|
// source = polySource,
|
||||||
|
// color = colorTypeExpression,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,11 @@ kotlinxSerialization = "1.10.0"
|
||||||
ksp = "2.3.4"
|
ksp = "2.3.4"
|
||||||
ktor = "3.4.0"
|
ktor = "3.4.0"
|
||||||
logback = "1.5.32"
|
logback = "1.5.32"
|
||||||
mapsCompose = "6.12.2"
|
maplibre = "0.12.1"
|
||||||
material = "1.7.3"
|
material = "1.7.3"
|
||||||
material3 = "1.11.0-alpha02"
|
material3 = "1.11.0-alpha02"
|
||||||
okio = "3.16.4"
|
okio = "3.16.4"
|
||||||
playServicesLocation = "21.3.0"
|
playServicesLocation = "21.3.0"
|
||||||
playServicesMaps = "19.2.0"
|
|
||||||
sqlite = "2.6.2"
|
sqlite = "2.6.2"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
secretsGradlePlugin = "2.0.1"
|
secretsGradlePlugin = "2.0.1"
|
||||||
|
|
@ -73,11 +72,9 @@ ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "kto
|
||||||
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
|
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
|
||||||
ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
|
ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
|
||||||
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
|
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre" }
|
||||||
maps-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "mapsCompose" }
|
|
||||||
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||||
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
|
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
|
||||||
play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" }
|
|
||||||
room-common = { group = "androidx.room", name = "room-common", version.ref = "room" }
|
room-common = { group = "androidx.room", name = "room-common", version.ref = "room" }
|
||||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue