refactor(ui): split into shared, maps, and main modules
This commit is contained in:
parent
aab03ced07
commit
a79c95829e
43 changed files with 539 additions and 378 deletions
|
|
@ -25,6 +25,7 @@ kotlin {
|
|||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||
freeCompilerArgs.add("-Xexplicit-backing-fields")
|
||||
}
|
||||
|
||||
listOf(
|
||||
|
|
@ -68,6 +69,8 @@ kotlin {
|
|||
|
||||
implementation(projects.client)
|
||||
implementation(projects.shared)
|
||||
implementation(projects.ui.maps)
|
||||
implementation(projects.ui.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,8 +82,3 @@ dependencies {
|
|||
secrets {
|
||||
propertiesFileName = "secrets.properties"
|
||||
}
|
||||
|
||||
compose.resources {
|
||||
publicResClass = true
|
||||
packageOfResClass = "moe.lava.banksia.resources"
|
||||
}
|
||||
|
|
|
|||
56
ui/maps/build.gradle.kts
Normal file
56
ui/maps/build.gradle.kts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "moe.lava.banksia.ui.map"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||
freeCompilerArgs.add("-Xexplicit-backing-fields")
|
||||
}
|
||||
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.play.services.location)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
|
||||
implementation(libs.maplibre.compose)
|
||||
implementation(libs.moko.geo)
|
||||
implementation(libs.moko.geo.compose)
|
||||
|
||||
implementation(libs.compose.components.resources)
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.ui)
|
||||
|
||||
implementation(projects.shared)
|
||||
implementation(projects.ui.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package moe.lava.banksia.ui.map
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.add
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import moe.lava.banksia.ui.map.mappers.routeColorExpression
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
import org.maplibre.compose.expressions.dsl.const
|
||||
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.Feature
|
||||
import org.maplibre.spatialk.geojson.Geometry
|
||||
|
||||
@Composable
|
||||
internal fun MapLibreMaps(
|
||||
modifier: Modifier,
|
||||
insets: WindowInsets,
|
||||
positionState: MapsPositionState,
|
||||
stops: GeoJsonData.Features?,
|
||||
// vehicles: GeoJsonData.Features?,
|
||||
stopInnerColor: Color,
|
||||
onStopClicked: (Feature<Geometry, JsonObject?>) -> Unit,
|
||||
) {
|
||||
val camPos = rememberCameraState(
|
||||
CameraPosition(
|
||||
zoom = 16.0,
|
||||
target = MELBOURNE_POS
|
||||
)
|
||||
)
|
||||
|
||||
MaplibreMap(
|
||||
modifier = modifier,
|
||||
baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/positron"),
|
||||
cameraState = camPos,
|
||||
options = MapOptions(
|
||||
ornamentOptions = OrnamentOptions(
|
||||
padding = WindowInsets.safeDrawing.add(insets).asPaddingValues(),
|
||||
isScaleBarEnabled = false,
|
||||
isAttributionEnabled = false,
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (stops != null) {
|
||||
val stopsSource = rememberGeoJsonSource(stops)
|
||||
CircleLayer(
|
||||
id = "maps-stops0",
|
||||
source = stopsSource,
|
||||
color = const(BanksiaTheme.colors.surface),
|
||||
radius = const(3.dp),
|
||||
strokeWidth = const(2.dp),
|
||||
strokeColor = routeColorExpression,
|
||||
)
|
||||
CircleLayer(
|
||||
id = "maps-stops0-clickhandler",
|
||||
source = stopsSource,
|
||||
color = const(Color.Transparent),
|
||||
radius = const(12.dp),
|
||||
onClick = { features ->
|
||||
// onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content))
|
||||
// val marker = Json.decodeFromJsonElement<T>(feature.properties!!)
|
||||
onStopClicked(features[0])
|
||||
ClickResult.Consume
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package moe.lava.banksia.ui.map
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import moe.lava.banksia.ui.map.mappers.asFeatures
|
||||
import moe.lava.banksia.ui.map.mappers.toPosition
|
||||
import moe.lava.banksia.ui.map.util.Marker
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
internal val MELBOURNE = Point(-37.8136, 144.9631)
|
||||
internal val MELBOURNE_POS = MELBOURNE.toPosition()
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Maps(
|
||||
modifier: Modifier = Modifier,
|
||||
insets: WindowInsets = WindowInsets(),
|
||||
stops: List<Marker.Stop> = listOf(),
|
||||
// vehicles: List<Marker.Vehicle> = listOf(),
|
||||
positionState: MapsPositionState = rememberMapsPositionState(),
|
||||
onStopClicked: (id: String) -> Unit = {},
|
||||
// onVehicleClicked: (id: String) -> Unit = {},
|
||||
) {
|
||||
MapLibreMaps(
|
||||
modifier = modifier,
|
||||
insets = insets,
|
||||
positionState = positionState,
|
||||
stops = stops.takeIf { it.isNotEmpty() }?.asFeatures(),
|
||||
// vehicles = vehicles.takeIf { it.isNotEmpty() }?.asFeatures(),
|
||||
stopInnerColor = BanksiaTheme.colors.surface,
|
||||
onStopClicked = { feature -> onStopClicked(feature.id!!.content) },
|
||||
// onVehicleClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package moe.lava.banksia.ui.map
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
class MapsPositionState internal constructor(
|
||||
private val scope: CoroutineScope
|
||||
) {
|
||||
internal val updates: SharedFlow<Point>
|
||||
field = MutableSharedFlow()
|
||||
|
||||
fun update(position: Point) {
|
||||
scope.launch { updates.emit(position) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberMapsPositionState(): MapsPositionState {
|
||||
val scope = rememberCoroutineScope()
|
||||
return remember { MapsPositionState(scope) }
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package moe.lava.banksia.ui.map.mappers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.ui.map.util.Marker
|
||||
import org.maplibre.compose.sources.GeoJsonData
|
||||
import org.maplibre.spatialk.geojson.FeatureCollection
|
||||
import org.maplibre.spatialk.geojson.dsl.addFeature
|
||||
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
|
||||
import org.maplibre.spatialk.geojson.Point as MLPoint
|
||||
|
||||
@Serializable
|
||||
data class MarkerProps(
|
||||
val type: RouteType,
|
||||
)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
internal inline fun Iterable<Marker>.asFeatures() = GeoJsonData.Features(asFeatureCollection())
|
||||
|
||||
internal fun Iterable<Marker>.asFeatureCollection(): FeatureCollection<MLPoint, MarkerProps> {
|
||||
val markers = this
|
||||
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.toPosition()),
|
||||
properties = MarkerProps(type),
|
||||
) {
|
||||
setId(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package moe.lava.banksia.ui.map.mappers
|
||||
|
||||
import moe.lava.banksia.util.Point
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
|
||||
internal fun Point.toPosition() = Position(lng, lat)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package moe.lava.banksia.ui.map.mappers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.ui.extensions.getUIProperties
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
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
|
||||
|
||||
internal val routeColorExpression @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),
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package moe.lava.banksia.ui.utils.map
|
||||
package moe.lava.banksia.ui.map.util
|
||||
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package moe.lava.banksia.ui.utils.map
|
||||
package moe.lava.banksia.ui.map.util
|
||||
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package moe.lava.banksia.ui.map.util
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
@Serializable
|
||||
sealed class Marker {
|
||||
abstract val point: Point
|
||||
|
||||
sealed class Typed : Marker() {
|
||||
abstract val type: RouteType
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Stop(
|
||||
override val point: Point,
|
||||
override val type: RouteType,
|
||||
val id: String,
|
||||
) : Typed()
|
||||
|
||||
@Serializable
|
||||
data class Vehicle(
|
||||
override val point: Point,
|
||||
override val type: RouteType,
|
||||
val ref: String,
|
||||
) : Typed()
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package moe.lava.banksia.ui.utils.map
|
||||
package moe.lava.banksia.ui.map.util
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import moe.lava.banksia.util.Point
|
||||
50
ui/shared/build.gradle.kts
Normal file
50
ui/shared/build.gradle.kts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "moe.lava.banksia.ui.shared"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||
freeCompilerArgs.add("-Xexplicit-backing-fields")
|
||||
}
|
||||
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.compose.components.resources)
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
|
||||
implementation(projects.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidRuntimeClasspath(libs.compose.ui.tooling)
|
||||
}
|
||||
|
||||
compose.resources {
|
||||
publicResClass = true
|
||||
packageOfResClass = "moe.lava.banksia.resources"
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package moe.lava.banksia.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.model.RouteType.MetroBus
|
||||
import moe.lava.banksia.model.RouteType.MetroTrain
|
||||
import moe.lava.banksia.model.RouteType.MetroTram
|
||||
import moe.lava.banksia.ui.extensions.getUIProperties
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
fun RouteIcon(
|
||||
modifier: Modifier = Modifier.Companion,
|
||||
size: Dp = 40.dp,
|
||||
routeType: RouteType,
|
||||
) {
|
||||
val properties = routeType.getUIProperties()
|
||||
Image(
|
||||
painter = painterResource(properties.icon),
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.aspectRatio(1f)
|
||||
.padding(size * ICON_PADDING / 2)
|
||||
.drawBehind {
|
||||
drawCircle(properties.colour, radius = size.toPx() / 2f)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const val ICON_PADDING = 0.25f
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RouteIconPreview() {
|
||||
Row {
|
||||
RouteIcon(routeType = MetroTrain)
|
||||
RouteIcon(routeType = MetroTram)
|
||||
RouteIcon(routeType = MetroBus)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,27 +1,8 @@
|
|||
package moe.lava.banksia.ui.components
|
||||
package moe.lava.banksia.ui.extensions
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.model.RouteType.Interstate
|
||||
import moe.lava.banksia.model.RouteType.MetroBus
|
||||
import moe.lava.banksia.model.RouteType.MetroTrain
|
||||
import moe.lava.banksia.model.RouteType.MetroTram
|
||||
import moe.lava.banksia.model.RouteType.RegionalBus
|
||||
import moe.lava.banksia.model.RouteType.RegionalCoach
|
||||
import moe.lava.banksia.model.RouteType.RegionalTrain
|
||||
import moe.lava.banksia.model.RouteType.SkyBus
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.bus
|
||||
import moe.lava.banksia.resources.bus_background
|
||||
|
|
@ -33,7 +14,6 @@ import moe.lava.banksia.resources.tram
|
|||
import moe.lava.banksia.resources.tram_background
|
||||
import moe.lava.banksia.resources.tram_icon
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
data class RouteTypeProperties(
|
||||
val colour: Color,
|
||||
|
|
@ -49,31 +29,31 @@ const val VLINE_PURPLE = 0xFF8F1A95
|
|||
|
||||
fun RouteType.getUIProperties(): RouteTypeProperties {
|
||||
val colour = when (this) {
|
||||
MetroTrain -> TRAIN_BLUE
|
||||
MetroTram -> TRAM_GREEN
|
||||
MetroBus -> BUS_ORANGE
|
||||
RegionalTrain -> VLINE_PURPLE
|
||||
RegionalCoach -> VLINE_PURPLE
|
||||
RegionalBus -> VLINE_PURPLE
|
||||
SkyBus -> BUS_ORANGE
|
||||
Interstate -> BUS_ORANGE
|
||||
RouteType.MetroTrain -> TRAIN_BLUE
|
||||
RouteType.MetroTram -> TRAM_GREEN
|
||||
RouteType.MetroBus -> BUS_ORANGE
|
||||
RouteType.RegionalTrain -> VLINE_PURPLE
|
||||
RouteType.RegionalCoach -> VLINE_PURPLE
|
||||
RouteType.RegionalBus -> VLINE_PURPLE
|
||||
RouteType.SkyBus -> BUS_ORANGE
|
||||
RouteType.Interstate -> BUS_ORANGE
|
||||
}
|
||||
|
||||
val (drawable, background, icon) = when (this) {
|
||||
MetroTrain,
|
||||
RegionalTrain,
|
||||
Interstate -> Triple(
|
||||
RouteType.MetroTrain,
|
||||
RouteType.RegionalTrain,
|
||||
RouteType.Interstate -> Triple(
|
||||
Res.drawable.train, Res.drawable.train_background, Res.drawable.train_icon
|
||||
)
|
||||
|
||||
MetroTram -> Triple(
|
||||
RouteType.MetroTram -> Triple(
|
||||
Res.drawable.tram, Res.drawable.tram_background, Res.drawable.tram_icon
|
||||
)
|
||||
|
||||
MetroBus,
|
||||
RegionalCoach,
|
||||
RegionalBus,
|
||||
SkyBus -> Triple(
|
||||
RouteType.MetroBus,
|
||||
RouteType.RegionalCoach,
|
||||
RouteType.RegionalBus,
|
||||
RouteType.SkyBus -> Triple(
|
||||
Res.drawable.bus, Res.drawable.bus_background, Res.drawable.bus_icon
|
||||
)
|
||||
}
|
||||
|
|
@ -102,35 +82,3 @@ fun PtvRouteType.getUIProperties(): RouteTypeProperties {
|
|||
return RouteTypeProperties(colour, drawable, background, icon)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RouteIcon(
|
||||
modifier: Modifier = Modifier.Companion,
|
||||
size: Dp = 40.dp,
|
||||
routeType: RouteType,
|
||||
) {
|
||||
val properties = routeType.getUIProperties()
|
||||
Image(
|
||||
painter = painterResource(properties.icon),
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.aspectRatio(1f)
|
||||
.padding(size * ICON_PADDING / 2)
|
||||
.drawBehind {
|
||||
drawCircle(properties.colour, radius = size.toPx() / 2f)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const val ICON_PADDING = 0.25f
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RouteIconPreview() {
|
||||
Row {
|
||||
RouteIcon(routeType = MetroTrain)
|
||||
RouteIcon(routeType = MetroTram)
|
||||
RouteIcon(routeType = MetroBus)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
|
||||
|
||||
</vector>
|
||||
|
|
@ -38,14 +38,12 @@ import moe.lava.banksia.ui.layout.AppBottomSheet
|
|||
import moe.lava.banksia.ui.layout.InfoPanel
|
||||
import moe.lava.banksia.ui.layout.Searcher
|
||||
import moe.lava.banksia.ui.layout.SheetStateWrapper
|
||||
import moe.lava.banksia.ui.map.Maps
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
import moe.lava.banksia.util.Point
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
val MELBOURNE = Point(-37.8136, 144.9631)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun MapScreen(
|
||||
|
|
@ -78,14 +76,20 @@ fun MapScreen(
|
|||
Scaffold {
|
||||
Maps(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = mapState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
cameraPositionFlow = viewModel.cameraChangeEmitter,
|
||||
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
||||
insets = WindowInsets(top = with(LocalDensity.current) {
|
||||
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||
}, bottom = sheetState.bottomInset),
|
||||
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||
stops = mapState.stops,
|
||||
// vehicles = mapState.vehicles,
|
||||
onStopClicked = { stop ->
|
||||
viewModel.handleEvent(MapScreenEvent.SelectStop(stop))
|
||||
},
|
||||
// onEvent = viewModel::handleEvent,
|
||||
// cameraPositionFlow = viewModel.cameraChangeEmitter,
|
||||
// setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||
)
|
||||
|
||||
// onEvent()
|
||||
Searcher(
|
||||
state = searchState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
|
|
|
|||
|
|
@ -15,39 +15,35 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.client.repository.RouteRepository
|
||||
import moe.lava.banksia.client.repository.StopRepository
|
||||
import moe.lava.banksia.client.repository.StopTimeRepository
|
||||
import moe.lava.banksia.data.ptv.PtvService
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
||||
import moe.lava.banksia.model.Route
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.ui.components.getUIProperties
|
||||
import moe.lava.banksia.ui.map.util.CameraPosition
|
||||
import moe.lava.banksia.ui.map.util.CameraPositionBounds
|
||||
import moe.lava.banksia.ui.map.util.Marker
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.ui.state.SearchState
|
||||
import moe.lava.banksia.ui.utils.map.CameraPosition
|
||||
import moe.lava.banksia.ui.utils.map.CameraPositionBounds
|
||||
import moe.lava.banksia.ui.utils.map.Marker
|
||||
import moe.lava.banksia.ui.utils.map.Polyline
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import moe.lava.banksia.util.BoxedValue.Companion.box
|
||||
import moe.lava.banksia.util.LoopFlow.Companion.waitUntilSubscribed
|
||||
import moe.lava.banksia.util.Point
|
||||
import moe.lava.banksia.util.log
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
sealed class MapScreenEvent {
|
||||
data object DismissState : MapScreenEvent()
|
||||
|
||||
data class SelectRoute(val id: String?) : MapScreenEvent()
|
||||
data class SelectRun(val ref: String?) : MapScreenEvent()
|
||||
data class SelectStop(val typeIdPair: Pair<RouteType, String>?) : MapScreenEvent()
|
||||
data class SelectStop(val id: String?) : MapScreenEvent()
|
||||
|
||||
data class SearchUpdate(val text: String) : MapScreenEvent()
|
||||
}
|
||||
|
||||
data class InternalState(
|
||||
val route: String? = null,
|
||||
val stop: Pair<RouteType, String>? = null,
|
||||
val stop: String? = null,
|
||||
val run: String? = null,
|
||||
)
|
||||
|
||||
|
|
@ -55,6 +51,7 @@ class MapScreenViewModel(
|
|||
private val ptvService: PtvService,
|
||||
private val routeRepository: RouteRepository,
|
||||
private val stopRepository: StopRepository,
|
||||
private val stopTimeRepository: StopTimeRepository,
|
||||
) : ViewModel() {
|
||||
private var state = InternalState()
|
||||
set(value) {
|
||||
|
|
@ -92,7 +89,7 @@ class MapScreenViewModel(
|
|||
is MapScreenEvent.DismissState -> dismissState()
|
||||
is MapScreenEvent.SelectRoute -> state = InternalState(route = event.id)
|
||||
is MapScreenEvent.SelectRun -> state = state.copy(run = event.ref, stop = null)
|
||||
is MapScreenEvent.SelectStop -> state = state.copy(stop = event.typeIdPair, run = null)
|
||||
is MapScreenEvent.SelectStop -> state = state.copy(stop = event.id, run = null)
|
||||
is MapScreenEvent.SearchUpdate -> searchUpdate(event.text)
|
||||
}
|
||||
}
|
||||
|
|
@ -206,12 +203,11 @@ class MapScreenViewModel(
|
|||
}
|
||||
|
||||
// [TODO]: Cleanup
|
||||
private suspend fun switchStop(pair: Pair<RouteType, String>?) {
|
||||
if (pair == null) {
|
||||
private suspend fun switchStop(id: String?) {
|
||||
if (id == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
val (type, id) = pair
|
||||
|
||||
val stop = stopRepository.get(id)
|
||||
// val stop = ptvService.stop(routeType, stopId)
|
||||
|
|
@ -226,36 +222,30 @@ class MapScreenViewModel(
|
|||
)
|
||||
}
|
||||
|
||||
val res = ptvService.departures(type, stop.id)
|
||||
// Map<
|
||||
// Pair<DirectionId, RouteId>,
|
||||
// Pair<DirectionName, List<DepartureTimes>>
|
||||
// >
|
||||
val timetable = HashMap<Pair<Int, Int>, Pair<String, MutableList<String>>>()
|
||||
res.departures.forEach { dep ->
|
||||
val key = Pair(dep.directionId, dep.routeId)
|
||||
val direction = ptvService.direction(dep.directionId, dep.routeId)
|
||||
val route = res.routes[dep.routeId.toString()]
|
||||
val prefix = route?.let { if (it.routeNumber == "") "" else "${it.routeNumber} - " } ?: ""
|
||||
val element = timetable.getOrPut(key) { Pair(prefix + direction.directionName, mutableListOf()) }.second
|
||||
if (element.size >= 5)
|
||||
return@forEach
|
||||
|
||||
val date = Instant.parse(dep.estimatedDepartureUtc ?: dep.scheduledDepartureUtc)
|
||||
val min = (date - Clock.System.now()).inWholeMinutes
|
||||
if (min <= -5)
|
||||
return@forEach
|
||||
if (min >= 65)
|
||||
element.add("${((min + 30.0) / 60.0).toInt()}hr")
|
||||
else
|
||||
element.add("${min}mn")
|
||||
}
|
||||
val departures = timetable.values.sortedBy { it.first }.map { (name, list) ->
|
||||
if (list.isEmpty())
|
||||
InfoPanelState.Stop.Departure(name, "No departures")
|
||||
else
|
||||
InfoPanelState.Stop.Departure(name, list.joinToString(" | "))
|
||||
}
|
||||
val departures = stopTimeRepository.getForStop(id)
|
||||
.filter { it.headsign != null }
|
||||
.groupBy { it.headsign!! }
|
||||
.map { (headsign, stopTimes) ->
|
||||
InfoPanelState.Stop.Departure(headsign, "...")
|
||||
// TODO
|
||||
// val tmsF = stopTimes.map { time ->
|
||||
// val key = Pair(dep.directionId, dep.routeId)
|
||||
// val direction = ptvService.direction(dep.directionId, dep.routeId)
|
||||
// val route = res.routes[dep.routeId.toString()]
|
||||
// val prefix = route?.let { if (it.routeNumber == "") "" else "${it.routeNumber} - " } ?: ""
|
||||
// val element = timetable.getOrPut(key) { Pair(prefix + direction.directionName, mutableListOf()) }.second
|
||||
// if (element.size >= 5)
|
||||
// return@forEach
|
||||
//
|
||||
// val min = (time.departureTime.time - Clock.System.now()).inWholeMinutes
|
||||
// if (min <= -5)
|
||||
// return@forEach
|
||||
// if (min >= 65)
|
||||
// element.add("${((min + 30.0) / 60.0).toInt()}hr")
|
||||
// else
|
||||
// element.add("${min}mn")
|
||||
// }
|
||||
}
|
||||
iInfoState.update {
|
||||
if (it !is InfoPanelState.Stop)
|
||||
it
|
||||
|
|
@ -264,7 +254,7 @@ class MapScreenViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun buildPolylines(route: PtvRoute) {
|
||||
/*private suspend fun buildPolylines(route: PtvRoute) {
|
||||
val routeWithGeo = if (route.geopath.isEmpty())
|
||||
ptvService.route(route.routeId, true)
|
||||
else
|
||||
|
|
@ -294,9 +284,9 @@ class MapScreenViewModel(
|
|||
|
||||
iMapState.update { it.copy(polylines = polylines) }
|
||||
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
||||
}
|
||||
}*/
|
||||
|
||||
private fun buildRuns(route: PtvRoute) {
|
||||
/*private fun buildRuns(route: PtvRoute) {
|
||||
ptvService
|
||||
.runsFlow(route.routeId)
|
||||
.waitUntilSubscribed(iInfoState)
|
||||
|
|
@ -317,19 +307,16 @@ class MapScreenViewModel(
|
|||
iMapState.update { it.copy(vehicles = markers) }
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
}
|
||||
}*/
|
||||
|
||||
private suspend fun buildStops(route: Route) {
|
||||
val stops = stopRepository.getByRoute(route.id)
|
||||
val colour = route.type.getUIProperties().colour
|
||||
|
||||
val markers = stops
|
||||
.map { stop ->
|
||||
Marker.Stop(
|
||||
point = stop.pos,
|
||||
id = stop.id,
|
||||
colour = colour,
|
||||
type = route.type,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,210 +0,0 @@
|
|||
@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
|
||||
|
||||
package moe.lava.banksia.ui.screens.map
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.add
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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.state.MapState
|
||||
import moe.lava.banksia.ui.utils.map.CameraPosition
|
||||
import moe.lava.banksia.ui.utils.map.Marker
|
||||
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
|
||||
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)
|
||||
|
||||
@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
|
||||
fun Maps(
|
||||
modifier: Modifier,
|
||||
state: MapState,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
) {
|
||||
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
|
||||
if (pos.bounds != null) {
|
||||
val (northeast, southwest) = pos.bounds
|
||||
camPos.animateTo(
|
||||
boundingBox = BoundingBox(
|
||||
southwest.toPos(),
|
||||
northeast.toPos()
|
||||
)
|
||||
)
|
||||
} 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 = MLCameraPosition(
|
||||
// zoom = 16.0,
|
||||
// target = Position(it.longitude, it.latitude)
|
||||
// )
|
||||
// setLastKnownLocation(Point(it.latitude, it.longitude))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
) {
|
||||
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,
|
||||
)
|
||||
CircleLayer(
|
||||
id = "maps-stops0-clickhandler",
|
||||
source = stopsSource,
|
||||
color = const(Color.Transparent),
|
||||
radius = const(12.dp),
|
||||
onClick = { features ->
|
||||
val feature = features[0]
|
||||
val marker = Json.decodeFromJsonElement<MarkerProps>(feature.properties!!)
|
||||
onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content))
|
||||
ClickResult.Consume
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 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,
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package moe.lava.banksia.ui.state
|
||||
|
||||
import moe.lava.banksia.ui.utils.map.Marker
|
||||
import moe.lava.banksia.ui.utils.map.Polyline
|
||||
import moe.lava.banksia.ui.map.util.Marker
|
||||
import moe.lava.banksia.ui.map.util.Polyline
|
||||
|
||||
data class MapState(
|
||||
val stops: List<Marker.Stop> = listOf(),
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
package moe.lava.banksia.ui.utils.map
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
sealed class Marker {
|
||||
abstract val point: Point
|
||||
|
||||
data class Stop(
|
||||
override val point: Point,
|
||||
val id: String,
|
||||
val type: RouteType,
|
||||
val colour: Color,
|
||||
) : Marker()
|
||||
|
||||
data class Vehicle(
|
||||
override val point: Point,
|
||||
val ref: String,
|
||||
val type: RouteType,
|
||||
) : Marker()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue