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 {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
|
|
@ -39,9 +40,6 @@ kotlin {
|
|||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.play.services.location)
|
||||
implementation(libs.play.services.maps)
|
||||
implementation(libs.maps.compose)
|
||||
implementation(libs.maps.compose.utils)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(libs.compose.components.resources)
|
||||
|
|
@ -62,6 +60,7 @@ kotlin {
|
|||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.contentnegotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.maplibre.compose)
|
||||
implementation(libs.moko.geo)
|
||||
implementation(libs.moko.geo.compose)
|
||||
implementation(projects.shared)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
|
||||
|
||||
package moe.lava.banksia.ui.platform.maps
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
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.add
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
|
@ -27,28 +22,43 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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 moe.lava.banksia.R
|
||||
import moe.lava.banksia.ui.components.RouteIcon
|
||||
import kotlinx.serialization.Serializable
|
||||
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.screens.MELBOURNE
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
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
|
||||
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)
|
||||
@Composable
|
||||
actual fun Maps(
|
||||
|
|
@ -72,104 +116,110 @@ actual fun Maps(
|
|||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
) {
|
||||
val camPos = rememberCameraPositionState()
|
||||
val camPos = rememberCameraState(
|
||||
MLCameraPosition(
|
||||
zoom = 16.0,
|
||||
target = MELBOURNE.toPos()
|
||||
)
|
||||
)
|
||||
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
|
||||
LaunchedEffect(newCameraPos) {
|
||||
log("maps", "newPos ${newCameraPos?.value}")
|
||||
val pos = newCameraPos?.value ?: return@LaunchedEffect
|
||||
val update = if (pos.bounds != null) {
|
||||
if (pos.bounds != null) {
|
||||
val (northeast, southwest) = pos.bounds
|
||||
val bounds = LatLngBounds(
|
||||
southwest.toLatLng(),
|
||||
northeast.toLatLng()
|
||||
camPos.animateTo(
|
||||
boundingBox = BoundingBox(
|
||||
southwest.toPos(),
|
||||
northeast.toPos()
|
||||
)
|
||||
CameraUpdateFactory.newLatLngBounds(bounds, 150)
|
||||
} else
|
||||
CameraUpdateFactory.newLatLngZoom(pos.centre.toLatLng(), 16.0f)
|
||||
|
||||
camPos.animate(update, 1000)
|
||||
)
|
||||
} else {
|
||||
camPos.animateTo(MLCameraPosition(
|
||||
target = pos.centre.toPos(),
|
||||
zoom = 16.0,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
val ctx = LocalContext.current
|
||||
val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) }
|
||||
LaunchedEffect(Unit) {
|
||||
@SuppressLint("MissingPermission")
|
||||
fusedLocation.lastLocation.addOnSuccessListener {
|
||||
if (it != null) {
|
||||
camPos.position = com.google.android.gms.maps.model.CameraPosition(
|
||||
LatLng(
|
||||
it.latitude,
|
||||
it.longitude
|
||||
), 16.0f, 0.0f, 0.0f
|
||||
camPos.position = MLCameraPosition(
|
||||
zoom = 16.0,
|
||||
target = Position(it.longitude, it.latitude)
|
||||
)
|
||||
setLastKnownLocation(Point(it.latitude, it.longitude))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GoogleMap(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
cameraPositionState = camPos,
|
||||
mapColorScheme = if (isSystemInDarkTheme()) {
|
||||
ComposeMapColorScheme.DARK
|
||||
} else {
|
||||
ComposeMapColorScheme.LIGHT
|
||||
},
|
||||
properties = DefaultMapProperties.copy(
|
||||
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()
|
||||
MaplibreMap(
|
||||
modifier = modifier,
|
||||
baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/positron"),
|
||||
cameraState = camPos,
|
||||
options = MapOptions(
|
||||
ornamentOptions = OrnamentOptions(
|
||||
padding = WindowInsets.safeDrawing.add(extInsets).asPaddingValues(),
|
||||
isScaleBarEnabled = false,
|
||||
isAttributionEnabled = false,
|
||||
)
|
||||
)
|
||||
) {
|
||||
// [TODO]: Slight lag when routes with many stops such as the 901 bus is set
|
||||
for (marker in state.stops) {
|
||||
val state = rememberUpdatedMarkerState(marker.point.toLatLng())
|
||||
MarkerComposable(
|
||||
keys = arrayOf(marker),
|
||||
zIndex = 0f,
|
||||
state = state,
|
||||
onClick = {
|
||||
onEvent(MapScreenEvent.SelectStop(marker.type to marker.id))
|
||||
false
|
||||
if (state.stops.isNotEmpty()) {
|
||||
val stopsSource = rememberGeoJsonSource(
|
||||
GeoJsonData.Features(buildMarkers(state.stops))
|
||||
)
|
||||
CircleLayer(
|
||||
id = "maps-stops0",
|
||||
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
|
||||
}
|
||||
) {
|
||||
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"
|
||||
ktor = "3.4.0"
|
||||
logback = "1.5.32"
|
||||
mapsCompose = "6.12.2"
|
||||
maplibre = "0.12.1"
|
||||
material = "1.7.3"
|
||||
material3 = "1.11.0-alpha02"
|
||||
okio = "3.16.4"
|
||||
playServicesLocation = "21.3.0"
|
||||
playServicesMaps = "19.2.0"
|
||||
sqlite = "2.6.2"
|
||||
room = "2.8.4"
|
||||
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-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
|
||||
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
|
||||
maps-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "mapsCompose" }
|
||||
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre" }
|
||||
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-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" }
|
||||
room-common = { group = "androidx.room", name = "room-common", 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" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue