diff --git a/.gitignore b/.gitignore
index 408e3b0..975a370 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,5 +18,6 @@ captures
**/xcshareddata/WorkspaceSettings.xcsettings
secrets.properties
-shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt
-data/
+/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt
+/data/
+/data
diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
new file mode 100644
index 0000000..b8b100b
--- /dev/null
+++ b/androidApp/build.gradle.kts
@@ -0,0 +1,57 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.composeMultiplatform)
+ alias(libs.plugins.composeCompiler)
+}
+
+kotlin {
+ target {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+ }
+
+ compilerOptions {
+ freeCompilerArgs.add("-Xexplicit-backing-fields")
+ }
+
+ dependencies {
+ implementation(projects.ui)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.compose.ui.tooling.preview)
+ }
+}
+
+dependencies {
+ debugImplementation(libs.compose.ui.tooling)
+}
+
+android {
+ namespace = "moe.lava.banksia"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
+ defaultConfig {
+ applicationId = "moe.lava.banksia"
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ targetSdk = libs.versions.android.targetSdk.get().toInt()
+ versionCode = 1
+ versionName = "1.0"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
similarity index 91%
rename from composeApp/src/androidMain/AndroidManifest.xml
rename to androidApp/src/main/AndroidManifest.xml
index 928349e..16435e6 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -13,9 +13,6 @@
android:enableOnBackInvokedCallback="true"
android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
-
-
-
-
-
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/local/RouteLocalDataSource.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/local/RouteLocalDataSource.kt
deleted file mode 100644
index bfbb204..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/local/RouteLocalDataSource.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package moe.lava.banksia.client.datasource.local
-
-import moe.lava.banksia.model.Route
-import moe.lava.banksia.room.dao.RouteDao
-import moe.lava.banksia.room.entity.asEntity
-
-class RouteLocalDataSource(private val dao: RouteDao) {
- suspend fun get(id: String) = dao.get(id)
- suspend fun getAll() = dao.getAll()
- suspend fun save(vararg routes: Route) = dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray())
-}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/local/StopLocalDataSource.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/local/StopLocalDataSource.kt
deleted file mode 100644
index 1b418a0..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/local/StopLocalDataSource.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package moe.lava.banksia.client.datasource.local
-
-import moe.lava.banksia.model.Stop
-import moe.lava.banksia.room.dao.RouteDao
-import moe.lava.banksia.room.dao.StopDao
-import moe.lava.banksia.room.entity.asEntity
-
-class StopLocalDataSource(private val dao: StopDao, private val routeDao: RouteDao) {
- suspend fun get(id: String) = dao.get(id)
- suspend fun getByRoute(id: String) = routeDao.stops(id)
- suspend fun save(vararg stops: Stop) = dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray())
-}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/RouteRemoteDataSource.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/RouteRemoteDataSource.kt
deleted file mode 100644
index 861a3d8..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/RouteRemoteDataSource.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package moe.lava.banksia.client.datasource.remote
-
-import io.ktor.client.HttpClient
-import io.ktor.client.call.body
-import io.ktor.client.request.get
-import moe.lava.banksia.model.Route
-
-class RouteRemoteDataSource(val client: HttpClient) {
- suspend fun get(id: String) = client.get("routes/${id}").body()
- suspend fun getAll() = client.get("routes").body>()
-}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/repository/RouteRepository.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/repository/RouteRepository.kt
deleted file mode 100644
index 49e397d..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/repository/RouteRepository.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package moe.lava.banksia.client.repository
-
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import moe.lava.banksia.client.datasource.local.RouteLocalDataSource
-import moe.lava.banksia.client.datasource.remote.RouteRemoteDataSource
-
-class RouteRepository(
- private val local: RouteLocalDataSource,
- private val remote: RouteRemoteDataSource,
-) {
- private val mutex = Mutex()
- suspend fun getAll() = mutex.withLock {
- local
- .getAll()
- .map { it.asModel() }
- .ifEmpty {
- remote
- .getAll()
- .also { local.save(*it.toTypedArray()) }
- }
- }
-
- suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
-}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopRepository.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopRepository.kt
deleted file mode 100644
index c9eedce..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopRepository.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package moe.lava.banksia.client.repository
-
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import moe.lava.banksia.client.datasource.local.StopLocalDataSource
-import moe.lava.banksia.client.datasource.remote.StopRemoteDataSource
-
-class StopRepository(
- private val local: StopLocalDataSource,
- private val remote: StopRemoteDataSource,
-) {
- private val mutex = Mutex()
-
- suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
- suspend fun getByRoute(id: String) = mutex.withLock {
- local
- .getByRoute(id)
- .map { it.asModel() }
- .ifEmpty { null }
- ?: remote.getByRoute(id)
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt
deleted file mode 100644
index 8d525f3..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt
+++ /dev/null
@@ -1,177 +0,0 @@
-package moe.lava.banksia.ui.layout
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.scaleIn
-import androidx.compose.animation.scaleOut
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeContent
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.windowInsetsBottomHeight
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.LoadingIndicator
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.coerceAtMost
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.delay
-import moe.lava.banksia.ui.components.RouteIcon
-import moe.lava.banksia.ui.screens.map.MapScreenEvent
-import moe.lava.banksia.ui.state.InfoPanelState
-import kotlin.time.Duration.Companion.milliseconds
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun InfoPanel(
- state: InfoPanelState,
- onEvent: (MapScreenEvent) -> Unit,
- onPeekHeightChange: (Dp) -> Unit,
-) {
- if (state is InfoPanelState.None)
- return
-
- val localDensity = LocalDensity.current
- var delayedLoad by remember { mutableStateOf(false) }
-
- LaunchedEffect(state.loading) {
- if (state.loading) {
- delay(200.milliseconds)
- delayedLoad = true
- } else {
- delayedLoad = false
- }
- }
-
- Column(
- Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp)
- .onSizeChanged {
- onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
- }
- ) {
- Box {
- when (state) {
- is InfoPanelState.Route -> RouteInfoPanel(state, onEvent)
- is InfoPanelState.Stop -> StopInfoPanel(state, onEvent)
- is InfoPanelState.Run -> RunInfoPanel(state, onEvent)
- is InfoPanelState.None -> throw UnsupportedOperationException()
- }
-
- this@Column.AnimatedVisibility(
- modifier = Modifier.align(Alignment.TopEnd),
- visible = delayedLoad,
- label = "sheet-loading",
- enter = fadeIn() + scaleIn(),
- exit = fadeOut() + scaleOut(),
- ) {
- LoadingIndicator(
- modifier = Modifier.size(48.dp)
- )
- }
- }
- Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent))
- }
-}
-
-@Composable
-private inline fun RouteInfoPanel(
- state: InfoPanelState.Route,
- onEvent: (MapScreenEvent) -> Unit,
-) {
- Column(Modifier.fillMaxWidth()) {
- Row {
- RouteIcon(routeType = state.type)
- Text(
- state.name,
- style = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.SemiBold,
- textAlign = TextAlign.Start
- )
- }
- }
-}
-
-@Composable
-private inline fun RunInfoPanel(
- state: InfoPanelState.Run,
- onEvent: (MapScreenEvent) -> Unit,
-) {
- Column(Modifier.fillMaxWidth()) {
- Row {
- RouteIcon(routeType = state.type)
- Text(
- "${state.direction} via ${state.routeName ?: "..."}",
- style = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.SemiBold,
- textAlign = TextAlign.Start
- )
- }
- }
-}
-
-@Composable
-private inline fun StopInfoPanel(
- state: InfoPanelState.Stop,
- onEvent: (MapScreenEvent) -> Unit,
-) {
- Column(Modifier.fillMaxWidth()) {
- Text(
- state.name,
- style = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.SemiBold,
- textAlign = TextAlign.Start
- )
- state.subname?.let {
- Text(
- "/ $it",
- modifier = Modifier.padding(start = 5.dp),
- style = MaterialTheme.typography.titleSmall,
- color = Color.Gray,
- fontWeight = FontWeight.SemiBold,
- textAlign = TextAlign.Start
- )
- }
- state.departures?.let {
- Spacer(Modifier.height(5.dp))
- it.forEach { (name, formatted) ->
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- name,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.SemiBold
- )
- Text(
- formatted,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.padding(horizontal = 5.dp)
- )
- }
- }
- }
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/Maps.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/Maps.kt
deleted file mode 100644
index fe20f9f..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/Maps.kt
+++ /dev/null
@@ -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): FeatureCollection {
- 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>,
- 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(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(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,
-// )
-// }
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt
deleted file mode 100644
index b0acbec..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package moe.lava.banksia.ui.state
-
-import moe.lava.banksia.model.RouteType
-
-sealed class InfoPanelState {
- abstract val loading: Boolean
-
- data object None : InfoPanelState() {
- override val loading = false
- }
-
- data class Route(
- val name: String,
- val type: RouteType,
- ) : InfoPanelState() {
- override val loading = false
- }
-
- data class Run(
- val direction: String,
- val type: RouteType,
- val routeName: String? = null,
- ) : InfoPanelState() {
- override val loading = routeName == null
- }
-
- data class Stop(
- val id: String,
- val name: String,
- val subname: String? = null,
- val departures: List? = null,
- ) : InfoPanelState() {
- override val loading: Boolean
- get() = departures == null
-
- data class Departure(val directionName: String, val formattedTimes: String)
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Marker.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Marker.kt
deleted file mode 100644
index 2efe33d..0000000
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Marker.kt
+++ /dev/null
@@ -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()
-}
diff --git a/composeApp/src/swift/spmMaplibre/StartYourBridgeHere.swift b/composeApp/src/swift/spmMaplibre/StartYourBridgeHere.swift
deleted file mode 100644
index d53c4d7..0000000
--- a/composeApp/src/swift/spmMaplibre/StartYourBridgeHere.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-import Foundation
-/**
-This is a starting class to set up your bridge.
-Ensure that your class is public and has the @objcMembers / @objc annotation.
-This file has been created because the folder is empty.
-Ignore this file if you don't need it or set "spmforkmp.disableStartupFile=true" inside your gradle file
-**/
-
-/**
-@objcMembers public class StartHere: NSObject {
- public override init() {
- super.init()
- }
-}
-**/
\ No newline at end of file
diff --git a/shared/build.gradle.kts b/core/build.gradle.kts
similarity index 52%
rename from shared/build.gradle.kts
rename to core/build.gradle.kts
index 1f26a53..3dd2ee6 100644
--- a/shared/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -1,22 +1,16 @@
-import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
- alias(libs.plugins.androidLibrary)
- alias(libs.plugins.ksp)
- alias(libs.plugins.room)
- alias(libs.plugins.wire)
-}
-
-room {
- schemaDirectory("$projectDir/schemas")
+ alias(libs.plugins.androidMultiplatformLibrary)
}
kotlin {
- androidTarget {
- @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ android {
+ namespace = "moe.lava.banksia.core"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
@@ -26,7 +20,6 @@ kotlin {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
- iosX64()
iosArm64()
iosSimulatorArm64()
@@ -47,38 +40,9 @@ kotlin {
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
- implementation(libs.room.runtime)
- implementation(libs.sqlite.bundled)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}
-
-dependencies {
- add("kspAndroid", libs.room.compiler)
- add("kspIosX64", libs.room.compiler)
- add("kspIosArm64", libs.room.compiler)
- add("kspIosSimulatorArm64", libs.room.compiler)
- add("kspJvm", libs.room.compiler)
-}
-
-android {
- namespace = "moe.lava.banksia.shared"
- compileSdk = libs.versions.android.compileSdk.get().toInt()
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
- defaultConfig {
- minSdk = libs.versions.android.minSdk.get().toInt()
- }
-}
-
-wire {
- sourcePath {
- srcDir("src/commonMain/proto")
- }
- kotlin {}
-}
\ No newline at end of file
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
new file mode 100644
index 0000000..ecdba19
--- /dev/null
+++ b/core/data/build.gradle.kts
@@ -0,0 +1,64 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.androidMultiplatformLibrary)
+}
+
+kotlin {
+ android {
+ namespace = "moe.lava.banksia.core.data"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+ }
+
+ compilerOptions {
+ freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
+ }
+
+ iosArm64()
+ iosSimulatorArm64()
+
+ jvm()
+
+ sourceSets {
+ val clientMain by creating {
+ dependsOn(commonMain.get())
+ }
+
+ androidMain.get().dependsOn(clientMain)
+ iosArm64Main.get().dependsOn(clientMain)
+ iosSimulatorArm64Main.get().dependsOn(clientMain)
+
+ commonMain.dependencies {
+ implementation(libs.koin.core)
+ implementation(projects.core)
+ api(projects.core.stoptime)
+ }
+
+ androidMain.dependencies {
+ implementation(libs.koin.compose)
+ implementation(libs.ktor.client.okhttp)
+ }
+ commonMain.dependencies {
+ implementation(libs.okio)
+ implementation(libs.koin.core)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.contentnegotiation)
+ implementation(libs.ktor.serialization.kotlinx.json)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.datetime)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlinx.serialization.protobuf)
+
+ implementation(projects.core)
+ implementation(projects.core.sqld)
+ }
+ iosMain.dependencies {
+ implementation(libs.ktor.client.darwin)
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt
similarity index 59%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt
rename to core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt
index 2002745..104c6bc 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt
+++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.client.di
+package moe.lava.banksia.core.data
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpSend
@@ -7,21 +7,22 @@ import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.plugin
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
-import moe.lava.banksia.Constants
-import moe.lava.banksia.client.datasource.local.RouteLocalDataSource
-import moe.lava.banksia.client.datasource.local.StopLocalDataSource
-import moe.lava.banksia.client.datasource.remote.RouteRemoteDataSource
-import moe.lava.banksia.client.datasource.remote.StopRemoteDataSource
-import moe.lava.banksia.client.repository.RouteRepository
-import moe.lava.banksia.client.repository.StopRepository
+import moe.lava.banksia.core.Constants
+import moe.lava.banksia.core.data.repositories.ClientRouteRepository
+import moe.lava.banksia.core.data.repositories.ClientStopRepository
+import moe.lava.banksia.core.data.repositories.RouteRepository
+import moe.lava.banksia.core.data.repositories.StopRepository
+import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
+import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
+import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
+import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
+import moe.lava.banksia.core.util.log
import moe.lava.banksia.data.ptv.PtvService
-import moe.lava.banksia.ui.screens.map.MapScreenViewModel
-import moe.lava.banksia.util.log
import org.koin.core.module.dsl.singleOf
-import org.koin.core.module.dsl.viewModelOf
+import org.koin.dsl.bind
import org.koin.dsl.module
-val ClientModule = module {
+actual val platformModule = module {
// HTTP Clients
singleOf(::PtvService)
single {
@@ -50,9 +51,6 @@ val ClientModule = module {
singleOf(::StopRemoteDataSource)
// Repositories
- singleOf(::RouteRepository)
- singleOf(::StopRepository)
-
- // ViewModel
- viewModelOf(::MapScreenViewModel)
+ singleOf(::ClientRouteRepository) bind RouteRepository::class
+ singleOf(::ClientStopRepository) bind StopRepository::class
}
diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt
new file mode 100644
index 0000000..f46caac
--- /dev/null
+++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt
@@ -0,0 +1,36 @@
+package moe.lava.banksia.core.data.repositories
+
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
+import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
+import moe.lava.banksia.core.model.Route
+import moe.lava.banksia.core.sqld.mappers.asModel
+
+internal class ClientRouteRepository internal constructor(
+ private val local: RouteLocalDataSource,
+ private val remote: RouteRemoteDataSource,
+) : RouteRepository {
+ private val mutex = Mutex()
+ override suspend fun getAll() = mutex.withLock {
+ local
+ .getAll()
+ .map { it.asModel() }
+ .ifEmpty {
+ remote
+ .getAll()
+ .also { local.save(*it.toTypedArray()) }
+ }
+ }
+
+ private val tripRouteMap = mutableMapOf()
+
+ override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
+ override suspend fun getByPattern(patternId: Long) = mutex.withLock {
+ tripRouteMap[patternId]
+ ?: remote.getByPattern(patternId).also {
+ local.save(it)
+ tripRouteMap[patternId] = it
+ }
+ }
+}
diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt
new file mode 100644
index 0000000..0aee84e
--- /dev/null
+++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt
@@ -0,0 +1,23 @@
+package moe.lava.banksia.core.data.repositories
+
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
+import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
+import moe.lava.banksia.core.sqld.mappers.asModel
+
+internal class ClientStopRepository internal constructor(
+ private val local: StopLocalDataSource,
+ private val remote: StopRemoteDataSource,
+) : StopRepository {
+ private val mutex = Mutex()
+
+ override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
+ override suspend fun getByRoute(id: String) = mutex.withLock {
+ local
+ .getByRoute(id)
+ .map { it.asModel() }
+ .ifEmpty { null }
+ ?: remote.getByRoute(id)
+ }
+}
diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt
new file mode 100644
index 0000000..8286b1f
--- /dev/null
+++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt
@@ -0,0 +1,23 @@
+package moe.lava.banksia.core.data.sources.route
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import kotlinx.coroutines.withContext
+import moe.lava.banksia.core.model.Route
+import moe.lava.banksia.core.sqld.RouteQueries
+import moe.lava.banksia.core.sqld.mappers.asDb
+
+internal class RouteLocalDataSource(private val queries: RouteQueries) {
+ suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() }
+ suspend fun getAll() = withContext(Dispatchers.IO) { queries.getAll().executeAsList() }
+// suspend fun getByTrip(tripId: String) = dao.getByTrip(tripId)
+ suspend fun save(vararg routes: Route) {
+ withContext(Dispatchers.IO) {
+ queries.transaction {
+ routes.forEach {
+ queries.insert(it.asDb())
+ }
+ }
+ }
+ }
+}
diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt
new file mode 100644
index 0000000..15088fb
--- /dev/null
+++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt
@@ -0,0 +1,12 @@
+package moe.lava.banksia.core.data.sources.route
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.get
+import moe.lava.banksia.core.model.Route
+
+internal class RouteRemoteDataSource(val client: HttpClient) {
+ suspend fun get(id: String) = client.get("routes/${id}").body()
+ suspend fun getByPattern(patternId: Long) = client.get("routes/by_pattern/${patternId}").body()
+ suspend fun getAll() = client.get("routes").body>()
+}
diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt
new file mode 100644
index 0000000..524d123
--- /dev/null
+++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt
@@ -0,0 +1,22 @@
+package moe.lava.banksia.core.data.sources.stop
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import kotlinx.coroutines.withContext
+import moe.lava.banksia.core.model.Stop
+import moe.lava.banksia.core.sqld.StopQueries
+import moe.lava.banksia.core.sqld.mappers.asDb
+
+internal class StopLocalDataSource(private val queries: StopQueries) {
+ suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() }
+ suspend fun getByRoute(id: String) = withContext(Dispatchers.IO) { queries.getByRoute(id).executeAsList() }
+ suspend fun save(vararg stops: Stop) {
+ withContext(Dispatchers.IO) {
+ queries.transaction {
+ stops.forEach {
+ queries.insert(it.asDb())
+ }
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/StopRemoteDataSource.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt
similarity index 64%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/StopRemoteDataSource.kt
rename to core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt
index f708cec..f39afd3 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/StopRemoteDataSource.kt
+++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt
@@ -1,11 +1,11 @@
-package moe.lava.banksia.client.datasource.remote
+package moe.lava.banksia.core.data.sources.stop
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
-import moe.lava.banksia.model.Stop
+import moe.lava.banksia.core.model.Stop
-class StopRemoteDataSource(val client: HttpClient) {
+internal class StopRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("stops/${id}").body()
suspend fun getByRoute(id: String) = client.get("route_stops/${id}").body>()
}
diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt
new file mode 100644
index 0000000..eea6a0e
--- /dev/null
+++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt
@@ -0,0 +1,13 @@
+package moe.lava.banksia.core.data
+
+import moe.lava.banksia.core.sqld.sqldDiModule
+import org.koin.core.module.Module
+import org.koin.dsl.module
+
+internal expect val platformModule: Module
+
+val dataDiModule = module {
+ includes(platformModule)
+ includes(sqldDiModule)
+ includes(stopTimeDataDiModule)
+}
diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt
new file mode 100644
index 0000000..ef3d6f1
--- /dev/null
+++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt
@@ -0,0 +1,9 @@
+package moe.lava.banksia.core.data.repositories
+
+import moe.lava.banksia.core.model.Route
+
+interface RouteRepository {
+ suspend fun get(id: String): Route?
+ suspend fun getByPattern(patternId: Long): Route?
+ suspend fun getAll(): List
+}
diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt
new file mode 100644
index 0000000..c663f89
--- /dev/null
+++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt
@@ -0,0 +1,8 @@
+package moe.lava.banksia.core.data.repositories
+
+import moe.lava.banksia.core.model.Stop
+
+interface StopRepository {
+ suspend fun get(id: String): Stop
+ suspend fun getByRoute(id: String): List
+}
diff --git a/core/data/src/jvmMain/kotlin/moe/lava/banksia/core/data/DataDiModule.jvm.kt b/core/data/src/jvmMain/kotlin/moe/lava/banksia/core/data/DataDiModule.jvm.kt
new file mode 100644
index 0000000..78a44d1
--- /dev/null
+++ b/core/data/src/jvmMain/kotlin/moe/lava/banksia/core/data/DataDiModule.jvm.kt
@@ -0,0 +1,7 @@
+package moe.lava.banksia.core.data
+
+import org.koin.dsl.module
+
+internal actual val platformModule = module {
+
+}
diff --git a/core/sqld/build.gradle.kts b/core/sqld/build.gradle.kts
new file mode 100644
index 0000000..472a908
--- /dev/null
+++ b/core/sqld/build.gradle.kts
@@ -0,0 +1,53 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.kotlinSerialization)
+ alias(libs.plugins.androidMultiplatformLibrary)
+ alias(libs.plugins.sqldelight)
+}
+
+kotlin {
+ android {
+ namespace = "moe.lava.banksia.core.sqld"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+ }
+
+ iosArm64()
+ iosSimulatorArm64()
+
+ jvm()
+
+ sourceSets {
+ androidMain.dependencies {
+ implementation(libs.sqldelight.driver.android)
+ }
+ commonMain.dependencies {
+ implementation(libs.okio)
+ implementation(libs.koin.core)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.datetime)
+
+ implementation(projects.core)
+ }
+ nativeMain.dependencies {
+ implementation(libs.sqldelight.driver.native)
+ }
+ jvmMain.dependencies {
+ implementation(libs.sqldelight.driver.jvm)
+ }
+ }
+}
+
+sqldelight {
+ databases {
+ register("BanksiaDatabase") {
+ packageName.set("moe.lava.banksia.core.sqld")
+ schemaOutputDirectory.set(file("src/commonMain/sqldelight/schema"))
+ }
+ }
+}
diff --git a/core/sqld/src/androidMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.android.kt b/core/sqld/src/androidMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.android.kt
new file mode 100644
index 0000000..c47613c
--- /dev/null
+++ b/core/sqld/src/androidMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.android.kt
@@ -0,0 +1,14 @@
+package moe.lava.banksia.core.sqld
+
+import android.content.Context
+import app.cash.sqldelight.driver.android.AndroidSqliteDriver
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+
+actual class DatabaseManager : KoinComponent {
+ actual val database by lazy {
+ val ctx = get().applicationContext
+ val driver = AndroidSqliteDriver(BanksiaDatabase.Schema, ctx, "${DBNAME}.db")
+ BanksiaDatabase(driver)
+ }
+}
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.kt
new file mode 100644
index 0000000..983eb58
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.kt
@@ -0,0 +1,7 @@
+package moe.lava.banksia.core.sqld
+
+internal const val DBNAME = "timetable"
+
+expect class DatabaseManager() {
+ val database: BanksiaDatabase
+}
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/SqldDiModule.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/SqldDiModule.kt
new file mode 100644
index 0000000..deee453
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/SqldDiModule.kt
@@ -0,0 +1,17 @@
+package moe.lava.banksia.core.sqld
+
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+
+val sqldDiModule = module {
+ singleOf(::DatabaseManager)
+ factory { get().database }
+ factory { get().routeQueries }
+ factory { get().serviceQueries }
+ factory { get().serviceExceptionQueries }
+ factory { get().shapeQueries }
+ factory { get().stopQueries }
+ factory { get().stoppingPatternQueries }
+ factory { get().stopTimeQueries }
+ factory { get().tripQueries }
+}
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Route.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Route.kt
new file mode 100644
index 0000000..f3a5521
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Route.kt
@@ -0,0 +1,14 @@
+package moe.lava.banksia.core.sqld.mappers
+
+import moe.lava.banksia.core.model.Route
+import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.core.sqld.Route as DbRoute
+
+fun DbRoute.asModel() = Route(
+ id = id,
+ type = RouteType.from(type.toInt()),
+ number = number,
+ name = name,
+)
+
+fun Route.asDb() = DbRoute(id, type.value.toLong(), number, name)
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Service.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Service.kt
new file mode 100644
index 0000000..dbda5ea
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Service.kt
@@ -0,0 +1,21 @@
+package moe.lava.banksia.core.sqld.mappers
+
+import kotlinx.datetime.LocalDate
+import moe.lava.banksia.core.model.Service
+import moe.lava.banksia.core.util.deserialiseDaysBitflag
+import moe.lava.banksia.core.util.serialise
+import moe.lava.banksia.core.sqld.Service as DbService
+
+fun DbService.asModel() = Service(
+ id = id,
+ days = days.toInt().deserialiseDaysBitflag(),
+ start = LocalDate.fromEpochDays(start),
+ end = LocalDate.fromEpochDays(end),
+)
+
+fun Service.asDb() = DbService(
+ id = id,
+ days = days.serialise().toLong(),
+ start = start.toEpochDays(),
+ end = end.toEpochDays(),
+)
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/ServiceException.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/ServiceException.kt
new file mode 100644
index 0000000..ef0d201
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/ServiceException.kt
@@ -0,0 +1,17 @@
+package moe.lava.banksia.core.sqld.mappers
+
+import kotlinx.datetime.LocalDate
+import moe.lava.banksia.core.model.ServiceException
+import moe.lava.banksia.core.sqld.ServiceException as DbServiceException
+
+fun DbServiceException.asModel() = ServiceException(
+ serviceId = serviceId,
+ date = LocalDate.fromEpochDays(date),
+ type = type.toInt(),
+)
+
+fun ServiceException.asDb() = DbServiceException(
+ serviceId = serviceId,
+ type = date.toEpochDays(),
+ date = type.toLong(),
+)
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Shape.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Shape.kt
new file mode 100644
index 0000000..4a8d7db
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Shape.kt
@@ -0,0 +1,52 @@
+package moe.lava.banksia.core.sqld.mappers
+
+import moe.lava.banksia.core.model.Shape
+import moe.lava.banksia.core.model.ShapePath
+import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.core.sqld.Shape as DbShape
+
+fun DbShape.asModel() = Shape(
+ id = id,
+ path = bytesToPath(path),
+)
+
+fun Shape.asDb() = DbShape(
+ id = id,
+ path = bytesFromPath(path),
+)
+
+private fun bytesToPath(value: ByteArray): ShapePath {
+ return value
+ .asSequence()
+ .asIterable()
+ .chunked(8) {
+ (it[0].toLong() and 0xFF) or
+ (it[1].toLong() and 0xFF shl 8) or
+ (it[2].toLong() and 0xFF shl 16) or
+ (it[3].toLong() and 0xFF shl 24) or
+ (it[4].toLong() and 0xFF shl 32) or
+ (it[5].toLong() and 0xFF shl 40) or
+ (it[6].toLong() and 0xFF shl 48) or
+ (it[7].toLong() and 0xFF shl 56)
+ }
+ .map { Double.fromBits(it) }
+ .chunked(2)
+ .map { (lat, lng) -> Point(lat, lng) }
+ .toList()
+}
+
+private fun bytesFromPath(path: ShapePath): ByteArray {
+ return path
+ .flatMap { (lat, lng) -> listOf(lat.toBits(), lng.toBits()) }
+ .flatMap { i -> listOf(
+ i.toByte(),
+ (i shr 8).toByte(),
+ (i shr 16).toByte(),
+ (i shr 24).toByte(),
+ (i shr 32).toByte(),
+ (i shr 40).toByte(),
+ (i shr 48).toByte(),
+ (i shr 56).toByte(),
+ ) }
+ .toByteArray()
+}
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Stop.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Stop.kt
new file mode 100644
index 0000000..3bf6b54
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Stop.kt
@@ -0,0 +1,26 @@
+package moe.lava.banksia.core.sqld.mappers
+
+import moe.lava.banksia.core.model.Stop
+import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.core.sqld.Stop as DbStop
+
+fun DbStop.asModel() = Stop(
+ id = id,
+ name = name,
+ pos = Point(lat, lng),
+ parent = parent,
+ hasWheelChairBoarding = hasWheelChairBoarding == 1L,
+ level = level,
+ platformCode = platformCode,
+)
+
+fun Stop.asDb() = DbStop(
+ id = id,
+ name = name,
+ lat = pos.lat,
+ lng = pos.lng,
+ parent = parent,
+ hasWheelChairBoarding = if (hasWheelChairBoarding) 1L else 0L,
+ level = level,
+ platformCode = platformCode
+)
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StopTime.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StopTime.kt
new file mode 100644
index 0000000..26d5390
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StopTime.kt
@@ -0,0 +1,27 @@
+package moe.lava.banksia.core.sqld.mappers
+
+import moe.lava.banksia.core.model.FutureTime
+import moe.lava.banksia.core.model.FutureTime.Companion.asInt
+import moe.lava.banksia.core.model.StopTime
+import moe.lava.banksia.core.model.TimeType
+import moe.lava.banksia.core.sqld.StopTime as DbStopTime
+
+fun DbStopTime.asModel() = StopTime(
+ patternId = patternId,
+ stopId = stopId,
+ time = TimeType.Undated(
+ arrival = FutureTime.fromInt((departureTime + arrivalDelta).toInt()),
+ departure = FutureTime.fromInt(departureTime.toInt()),
+ ),
+ pickupType = pickupType.toInt(),
+ dropOffType = dropOffType.toInt(),
+)
+
+fun StopTime.Undated.asDb() = DbStopTime(
+ patternId = patternId,
+ stopId = stopId,
+ arrivalDelta = (time.arrival.asInt() - time.departure.asInt()).toLong(),
+ departureTime = time.departure.asInt().toLong(),
+ pickupType = pickupType.toLong(),
+ dropOffType = dropOffType.toLong(),
+)
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StoppingPattern.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StoppingPattern.kt
new file mode 100644
index 0000000..d1409a2
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StoppingPattern.kt
@@ -0,0 +1,23 @@
+package moe.lava.banksia.core.sqld.mappers
+
+import moe.lava.banksia.core.model.StopTime
+import moe.lava.banksia.core.model.StoppingPattern
+import moe.lava.banksia.core.model.TimeType
+import moe.lava.banksia.core.sqld.StoppingPattern as DbStoppingPattern
+
+fun DbStoppingPattern.asModel(stoptimes: List>) = StoppingPattern(
+ id = id,
+ routeId = routeId,
+ shapeId = shapeId,
+ headsign = headsign,
+ wheelchairAccessible = wheelchairAccessible == 1L,
+ stoptimes = stoptimes,
+)
+
+fun StoppingPattern<*>.asDb() = DbStoppingPattern(
+ id = id,
+ routeId = routeId,
+ shapeId = shapeId,
+ headsign = headsign,
+ wheelchairAccessible = if (wheelchairAccessible) 1L else 0L,
+)
diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Trip.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Trip.kt
new file mode 100644
index 0000000..b3443fb
--- /dev/null
+++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Trip.kt
@@ -0,0 +1,27 @@
+package moe.lava.banksia.core.sqld.mappers
+
+import moe.lava.banksia.core.model.Service
+import moe.lava.banksia.core.model.StoppingPattern
+import moe.lava.banksia.core.model.Trip
+import moe.lava.banksia.core.sqld.Trip as DbTrip
+
+fun DbTrip.asModel(pattern: StoppingPattern.Undated, service: Service): Trip.Undated {
+ if (serviceId != service.id) {
+ throw IllegalArgumentException("trip and service id mismatch (${serviceId} != ${service.id})")
+ }
+ return Trip(
+ id = gtfsId,
+ pattern = pattern,
+ service = service,
+ directionId = directionId.toInt(),
+ blockId = blockId.toString(),
+ )
+}
+
+fun Trip.Undated.asDb() = DbTrip(
+ gtfsId = id,
+ patternId = pattern.id,
+ serviceId = service.id,
+ directionId = directionId.toLong(),
+ blockId = blockId?.toLong(),
+)
diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Route.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Route.sq
new file mode 100644
index 0000000..e607975
--- /dev/null
+++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Route.sq
@@ -0,0 +1,20 @@
+CREATE TABLE Route (
+ id TEXT PRIMARY KEY NOT NULL,
+ type INTEGER NOT NULL,
+ number TEXT,
+ name TEXT NOT NULL
+);
+
+getAll:
+SELECT * FROM Route;
+
+get:
+SELECT * FROM Route WHERE id == ?;
+
+getByPattern:
+SELECT Route.* FROM Route
+INNER JOIN StoppingPattern ON Route.id == StoppingPattern.routeId
+WHERE StoppingPattern.id == :patternId;
+
+insert:
+INSERT OR REPLACE INTO Route VALUES ?;
diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Service.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Service.sq
new file mode 100644
index 0000000..a1c5fad
--- /dev/null
+++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Service.sq
@@ -0,0 +1,11 @@
+CREATE TABLE Service (
+ id TEXT PRIMARY KEY NOT NULL,
+ days INTEGER NOT NULL,
+ start INTEGER NOT NULL,
+ end INTEGER NOT NULL
+);
+
+CREATE INDEX idx_Service_days ON Service (days);
+
+insert:
+INSERT INTO Service VALUES ?;
diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/ServiceException.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/ServiceException.sq
new file mode 100644
index 0000000..332f198
--- /dev/null
+++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/ServiceException.sq
@@ -0,0 +1,9 @@
+CREATE TABLE ServiceException (
+ serviceId TEXT NOT NULL,
+ type INTEGER NOT NULL,
+ date INTEGER NOT NULL,
+ PRIMARY KEY (serviceId, type)
+);
+
+insert:
+INSERT INTO ServiceException VALUES ?;
diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Shape.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Shape.sq
new file mode 100644
index 0000000..8734200
--- /dev/null
+++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Shape.sq
@@ -0,0 +1,7 @@
+CREATE TABLE Shape (
+ id TEXT PRIMARY KEY NOT NULL,
+ path BLOB NOT NULL
+);
+
+insert:
+INSERT INTO Shape VALUES ?;
diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Stop.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Stop.sq
new file mode 100644
index 0000000..4af5c50
--- /dev/null
+++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Stop.sq
@@ -0,0 +1,54 @@
+CREATE TABLE Stop (
+ id TEXT PRIMARY KEY NOT NULL,
+ name TEXT NOT NULL,
+ lat REAL NOT NULL,
+ lng REAL NOT NULL,
+ parent TEXT REFERENCES Stop(id),
+ hasWheelChairBoarding INTEGER NOT NULL,
+ level TEXT,
+ platformCode TEXT
+);
+
+CREATE INDEX idx_Stop_parent ON Stop (parent);
+
+getAll:
+SELECT * FROM Stop;
+
+getAllParentless:
+SELECT * FROM Stop WHERE platformCode IS NOT NULL AND parent IS NULL;
+
+get:
+SELECT * FROM Stop WHERE id == ?;
+
+getMany:
+SELECT * FROM Stop WHERE id IN ?;
+
+insert:
+INSERT INTO Stop VALUES ?;
+
+updateParents:
+UPDATE Stop SET parent = ? WHERE id IN ?;
+
+getByRoute:
+SELECT Stop.* FROM Stop
+INNER JOIN StopTime ON StopTime.stopId == Stop.id
+INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId
+WHERE StoppingPattern.routeId == :id
+GROUP BY Stop.id;
+
+-- I vibecoded this, sorry
+getParentsByRoute:
+WITH RECURSIVE Tree AS (
+ SELECT Stop.* FROM Stop
+ INNER JOIN StopTime ON StopTime.stopId == Stop.id
+ INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId
+ WHERE StoppingPattern.routeId == :id
+ GROUP BY Stop.id
+
+ UNION ALL
+
+ SELECT s.*
+ FROM Stop s
+ INNER JOIN Tree t ON s.id = t.parent
+)
+SELECT DISTINCT * FROM Tree WHERE parent IS NULL;
diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StopTime.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StopTime.sq
new file mode 100644
index 0000000..06bd76b
--- /dev/null
+++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StopTime.sq
@@ -0,0 +1,45 @@
+CREATE TABLE StopTime (
+ patternId INTEGER NOT NULL REFERENCES StoppingPattern (id),
+ stopId TEXT NOT NULL REFERENCES Stop (id),
+ arrivalDelta INTEGER NOT NULL,
+ departureTime INTEGER NOT NULL,
+ pickupType INTEGER NOT NULL,
+ dropOffType INTEGER NOT NULL,
+ PRIMARY KEY (patternId, stopId)
+) WITHOUT ROWID;
+
+CREATE INDEX idx_StopTime_stopId ON StopTime (stopId);
+
+insert:
+INSERT OR REPLACE INTO StopTime VALUES ?;
+
+getForStopDated:
+SELECT DISTINCT StopTime.* FROM StopTime
+INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
+LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
+INNER JOIN Trip ON Trip.serviceId == Service.id
+INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
+WHERE StopTime.patternId == StoppingPattern.id
+ AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
+ AND ServiceException.type IS NULL;
+
+getExtendedForStop:
+SELECT DISTINCT
+ StopTime.patternId,
+ StopTime.arrivalDelta,
+ StopTime.departureTime,
+ StoppingPattern.headsign,
+ Route.type AS routeType,
+ Route.number AS routeNumber,
+ Route.name AS routeName,
+ Stop.platformCode AS stopPlatformCode
+FROM StopTime
+INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
+LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
+INNER JOIN Trip ON Trip.serviceId == Service.id
+INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
+INNER JOIN Route ON Route.id == StoppingPattern.routeId
+INNER JOIN Stop ON Stop.id == StopTime.stopId
+WHERE StopTime.patternId == StoppingPattern.id
+ AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
+ AND ServiceException.type IS NULL;
diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StoppingPattern.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StoppingPattern.sq
new file mode 100644
index 0000000..9a09e69
--- /dev/null
+++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StoppingPattern.sq
@@ -0,0 +1,13 @@
+CREATE TABLE StoppingPattern (
+ id INTEGER PRIMARY KEY NOT NULL,
+ routeId TEXT NOT NULL REFERENCES Route (id),
+ shapeId TEXT NOT NULL REFERENCES Shape (id),
+ headsign TEXT NOT NULL,
+ wheelchairAccessible INTEGER NOT NULL
+);
+
+insert:
+INSERT OR REPLACE INTO StoppingPattern VALUES ?;
+
+get:
+SELECT * FROM StoppingPattern WHERE id == :id;
diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Trip.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Trip.sq
new file mode 100644
index 0000000..c53b62a
--- /dev/null
+++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Trip.sq
@@ -0,0 +1,13 @@
+CREATE TABLE Trip (
+ gtfsId TEXT PRIMARY KEY NOT NULL,
+ patternId INTEGER NOT NULL REFERENCES StoppingPattern (id),
+ serviceId TEXT NOT NULL REFERENCES Service (id),
+ blockId INTEGER,
+ directionId INTEGER NOT NULL
+);
+
+CREATE INDEX idx_Trip_patternId ON Trip (patternId);
+CREATE INDEX idx_Trip_serviceId ON Trip (serviceId);
+
+insert:
+INSERT OR REPLACE INTO Trip VALUES ?;
diff --git a/core/sqld/src/commonMain/sqldelight/schema/1.db b/core/sqld/src/commonMain/sqldelight/schema/1.db
new file mode 100644
index 0000000..feaacb3
Binary files /dev/null and b/core/sqld/src/commonMain/sqldelight/schema/1.db differ
diff --git a/core/sqld/src/iosMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.ios.kt b/core/sqld/src/iosMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.ios.kt
new file mode 100644
index 0000000..9ce0627
--- /dev/null
+++ b/core/sqld/src/iosMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.ios.kt
@@ -0,0 +1,11 @@
+package moe.lava.banksia.core.sqld
+
+import app.cash.sqldelight.driver.native.NativeSqliteDriver
+import org.koin.core.component.KoinComponent
+
+actual class DatabaseManager : KoinComponent {
+ actual val database by lazy {
+ val driver = NativeSqliteDriver(BanksiaDatabase.Schema, "${DBNAME}.db")
+ BanksiaDatabase(driver)
+ }
+}
diff --git a/core/sqld/src/jvmMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.jvm.kt b/core/sqld/src/jvmMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.jvm.kt
new file mode 100644
index 0000000..61d9e95
--- /dev/null
+++ b/core/sqld/src/jvmMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.jvm.kt
@@ -0,0 +1,56 @@
+package moe.lava.banksia.core.sqld
+
+import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import moe.lava.banksia.core.util.error
+import org.koin.core.component.KoinComponent
+import java.io.File
+import java.util.Properties
+import kotlin.system.exitProcess
+
+actual class DatabaseManager : KoinComponent {
+ private var driver = connect()
+ actual val database get() = BanksiaDatabase(driver)
+
+ private fun connect(path: String = "./data/${DBNAME}.db") =
+ JdbcSqliteDriver("jdbc:sqlite:${path}", Properties(), BanksiaDatabase.Schema)
+ .apply { execute(null, "PRAGMA journal_mode = OFF;", 0) }
+
+ fun makeAlt() = run {
+ File("./data/${DBNAME}_alt.db").takeIf { it.exists() }?.delete()
+ val driver = connect("./data/${DBNAME}_alt.db")
+ BanksiaDatabase(driver) to { driver.close() }
+ }
+
+ fun swap(scope: CoroutineScope = CoroutineScope(Dispatchers.IO)) {
+ val live = File("./data/${DBNAME}.db")
+ val alt = File("./data/${DBNAME}_alt.db")
+ val old = File("./data/${DBNAME}_old.db")
+
+ if (live.takeIf { it.exists() }?.renameTo(old) == false) {
+ error("DatabaseManager", "Failed to rename database from live to old (${live.absolutePath} -> ${old.absolutePath})")
+ return
+ }
+ if (alt.takeIf { it.exists() }?.renameTo(live) == false) {
+ error("DatabaseManager", "Failed to rename database from alt to live, trying to undo.. (${alt.absolutePath} -> ${live.absolutePath})")
+ if (!live.renameTo(old)) {
+ error("DatabaseManager", "Failed to undo, critical failure, exiting..")
+ exitProcess(1)
+ }
+ return
+ }
+ val oldDriver = driver
+ driver = connect()
+
+ scope.launch {
+ delay(5000)
+ if (old.takeIf { it.exists() }?.delete() == false) {
+ error("DatabaseManager", "Failed to unlink old database, stray files! (${old.absolutePath})")
+ }
+ oldDriver.close()
+ }
+ }
+}
diff --git a/shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt b/core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt
similarity index 87%
rename from shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt
rename to core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt
index 31c3072..e0b792e 100644
--- a/shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt
+++ b/core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.util
+package moe.lava.banksia.core.util
import android.util.Log
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton b/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton
similarity index 78%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton
index 7329ae3..909f642 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton
@@ -6,6 +6,7 @@ object Constants {
const val opendataKey: String = ""
const val serverUrl: String = "https://banksia.lava.moe/api/"
// TODO
- const val devMode: Boolean = false
+ var devMode: Boolean = false
const val updateKey: String = ""
+ const val protomapsKey: String = ""
}
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt
new file mode 100644
index 0000000..7e23b5d
--- /dev/null
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt
@@ -0,0 +1,3 @@
+package moe.lava.banksia.core.endpoints
+
+object Endpoint
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt
similarity index 81%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt
index c1853a9..7c77309 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt
@@ -1,6 +1,10 @@
-package moe.lava.banksia.model
+package moe.lava.banksia.core.model
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
+import kotlinx.datetime.atTime
+import kotlinx.datetime.plus
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
@@ -8,7 +12,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
-import moe.lava.banksia.model.FutureTime.Companion.asInt
+import moe.lava.banksia.core.model.FutureTime.Companion.asInt
@Serializable(FutureTimeSerialiser::class)
data class FutureTime(
@@ -39,6 +43,10 @@ data class FutureTime(
val minute = time.minute
val second = time.second
val trueHour = time.hour + (if (dayOffset) 24 else 0)
+
+ fun atDate(date: LocalDate) = date
+ .let { if (dayOffset) date.plus(1, DateTimeUnit.DAY) else date }
+ .atTime(time)
}
object FutureTimeSerialiser: KSerializer {
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt
similarity index 82%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt
index 9cfff0f..b2741f4 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.model
+package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt
similarity index 66%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt
index 08a9c53..86555a6 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.model
+package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
@@ -13,4 +13,8 @@ enum class RouteType(val value: Int) {
SkyBus(11),
Interstate(10),
;
+
+ companion object {
+ fun from(value: Int) = entries.first { it.value == value }
+ }
}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt
similarity index 52%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt
index 328a4b0..69799bf 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.model
+package moe.lava.banksia.core.model
data class Run(
val ref: String,
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt
similarity index 87%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt
index a57fb82..8568397 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.model
+package moe.lava.banksia.core.model
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/ServiceException.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/ServiceException.kt
new file mode 100644
index 0000000..ef2f918
--- /dev/null
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/ServiceException.kt
@@ -0,0 +1,11 @@
+package moe.lava.banksia.core.model
+
+import kotlinx.datetime.LocalDate
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ServiceException(
+ val serviceId: String,
+ val date: LocalDate,
+ val type: Int,
+)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt
similarity index 67%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt
index 6299ca0..7b71427 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt
@@ -1,7 +1,7 @@
-package moe.lava.banksia.model
+package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
-import moe.lava.banksia.util.Point
+import moe.lava.banksia.core.util.Point
typealias ShapePath = List
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt
similarity index 53%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt
index df10a58..bbe6fbf 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt
@@ -1,15 +1,15 @@
-package moe.lava.banksia.model
+package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
-import moe.lava.banksia.util.Point
+import moe.lava.banksia.core.util.Point
@Serializable
data class Stop(
val id: String,
val name: String,
val pos: Point,
- val parent: String,
+ val parent: String?,
val hasWheelChairBoarding: Boolean,
- val level: String,
- val platformCode: String,
+ val level: String?,
+ val platformCode: String?,
)
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt
new file mode 100644
index 0000000..edd7c51
--- /dev/null
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt
@@ -0,0 +1,45 @@
+package moe.lava.banksia.core.model
+
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class StopTime(
+ val patternId: Long,
+ val stopId: String,
+ val time: T,
+ val pickupType: Int,
+ val dropOffType: Int,
+) {
+ typealias Dated = StopTime
+ typealias Undated = StopTime
+}
+
+@Serializable
+sealed class TimeType {
+ @Serializable
+ data class Undated(
+ val arrival: FutureTime,
+ val departure: FutureTime,
+ ) : TimeType()
+
+ @Serializable
+ data class Dated(
+ val arrival: LocalDateTime,
+ val departure: LocalDateTime,
+ ) : TimeType()
+}
+
+fun TimeType.Undated.atDate(date: LocalDate) = TimeType.Dated(
+ arrival = arrival.atDate(date),
+ departure = departure.atDate(date),
+)
+
+fun StopTime.atDate(date: LocalDate) = StopTime(
+ patternId = patternId,
+ stopId = stopId,
+ time = time.atDate(date),
+ pickupType = pickupType,
+ dropOffType = dropOffType,
+)
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StoppingPattern.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StoppingPattern.kt
new file mode 100644
index 0000000..1374cff
--- /dev/null
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StoppingPattern.kt
@@ -0,0 +1,16 @@
+package moe.lava.banksia.core.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class StoppingPattern(
+ val id: Long,
+ val routeId: String,
+ val shapeId: String,
+ val headsign: String,
+ val wheelchairAccessible: Boolean,
+ val stoptimes: List>,
+) {
+ typealias Dated = StoppingPattern
+ typealias Undated = StoppingPattern
+}
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Trip.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Trip.kt
new file mode 100644
index 0000000..752d6d2
--- /dev/null
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Trip.kt
@@ -0,0 +1,15 @@
+package moe.lava.banksia.core.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Trip(
+ val id: String,
+ val pattern: StoppingPattern,
+ val service: Service,
+ val directionId: Int,
+ val blockId: String?,
+) {
+ typealias Dated = Trip
+ typealias Undated = Trip
+}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt
similarity index 79%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt
index 1770b23..2ee4f28 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.model
+package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt
similarity index 53%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt
index 0d6896d..f761518 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt
@@ -1,5 +1,6 @@
-package moe.lava.banksia.util
+package moe.lava.banksia.core.util
+/** Wraps an arbitrary value, such that equality checks are forced to be done by reference */
class BoxedValue(val value: T) {
operator fun component1() = value
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt
similarity index 97%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt
index e41cef6..22236c6 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.util
+package moe.lava.banksia.core.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt
new file mode 100644
index 0000000..7feca0d
--- /dev/null
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt
@@ -0,0 +1,36 @@
+package moe.lava.banksia.core.util
+
+import kotlinx.datetime.DayOfWeek
+
+private fun Int.check(other: Int) = (this and other) != 0
+
+fun Int.deserialiseDaysBitflag(): List = buildList {
+ val days = this@deserialiseDaysBitflag
+ if (days.check(1))
+ add(DayOfWeek.MONDAY)
+ if (days.check(1 shl 1))
+ add(DayOfWeek.TUESDAY)
+ if (days.check(1 shl 2))
+ add(DayOfWeek.WEDNESDAY)
+ if (days.check(1 shl 3))
+ add(DayOfWeek.THURSDAY)
+ if (days.check(1 shl 4))
+ add(DayOfWeek.FRIDAY)
+ if (days.check(1 shl 5))
+ add(DayOfWeek.SATURDAY)
+ if (days.check(1 shl 6))
+ add(DayOfWeek.SUNDAY)
+}
+
+fun List.serialise(): Int =
+ this.fold(0) { vl, n ->
+ vl + when (n) {
+ DayOfWeek.MONDAY -> 1
+ DayOfWeek.TUESDAY -> 1 shl 1
+ DayOfWeek.WEDNESDAY -> 1 shl 2
+ DayOfWeek.THURSDAY -> 1 shl 3
+ DayOfWeek.FRIDAY -> 1 shl 4
+ DayOfWeek.SATURDAY -> 1 shl 5
+ DayOfWeek.SUNDAY -> 1 shl 6
+ }
+ }
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt
similarity index 88%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt
index 7f26800..9d5f55a 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.util
+package moe.lava.banksia.core.util
fun error(tag: String, throwable: Throwable) = error(tag, "", throwable)
expect fun log(tag: String, msg: String)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt
similarity index 98%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt
index ee3e826..ec21d62 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.util
+package moe.lava.banksia.core.util
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt
similarity index 75%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt
index 4aae7d4..4db05e2 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.util
+package moe.lava.banksia.core.util
import kotlinx.serialization.Serializable
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
similarity index 97%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
index 77ab12d..54717a2 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
@@ -16,7 +16,12 @@ import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
-import moe.lava.banksia.Constants
+import moe.lava.banksia.core.Constants
+import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.core.util.LoopFlow.Companion.initWith
+import moe.lava.banksia.core.util.error
+import moe.lava.banksia.core.util.log
+import moe.lava.banksia.core.util.loopFlow
import moe.lava.banksia.data.ptv.structures.PtvDeparture
import moe.lava.banksia.data.ptv.structures.PtvDirection
import moe.lava.banksia.data.ptv.structures.PtvRoute
@@ -24,11 +29,6 @@ import moe.lava.banksia.data.ptv.structures.PtvRouteType
import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType
import moe.lava.banksia.data.ptv.structures.PtvRun
import moe.lava.banksia.data.ptv.structures.PtvStop
-import moe.lava.banksia.model.RouteType
-import moe.lava.banksia.util.LoopFlow.Companion.initWith
-import moe.lava.banksia.util.error
-import moe.lava.banksia.util.log
-import moe.lava.banksia.util.loopFlow
import okio.ByteString.Companion.encodeUtf8
import kotlin.random.Random
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
similarity index 94%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
index 3178328..4aae762 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
@@ -2,7 +2,7 @@ package moe.lava.banksia.data.ptv.structures
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-import moe.lava.banksia.model.RouteType
+import moe.lava.banksia.core.model.RouteType
@Serializable
data class PtvRoute(
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
similarity index 93%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
index 0726665..d8808f1 100644
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
+++ b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
@@ -7,9 +7,9 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
-import moe.lava.banksia.model.RouteType
+import moe.lava.banksia.core.model.RouteType
-private object PtvRouteTypeSerialiser : KSerializer {
+object PtvRouteTypeSerialiser : KSerializer {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
PtvRouteType::class.qualifiedName!!,
PrimitiveKind.INT)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt
rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt
diff --git a/core/src/iosMain/kotlin/moe/lava/banksia/core/util/Logging.ios.kt b/core/src/iosMain/kotlin/moe/lava/banksia/core/util/Logging.ios.kt
new file mode 100644
index 0000000..014c1d2
--- /dev/null
+++ b/core/src/iosMain/kotlin/moe/lava/banksia/core/util/Logging.ios.kt
@@ -0,0 +1,9 @@
+package moe.lava.banksia.core.util
+
+actual fun log(tag: String, msg: String) {
+ TODO("Not yet implemented")
+}
+
+actual fun error(tag: String, msg: String, throwable: Throwable?) {
+ TODO("Not yet implemented")
+}
diff --git a/shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt b/core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt
similarity index 86%
rename from shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt
rename to core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt
index 0a1ea10..de7fdaa 100644
--- a/shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt
+++ b/core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.util
+package moe.lava.banksia.core.util
actual fun log(tag: String, msg: String) {
println("[$tag] $msg")
diff --git a/core/stoptime/build.gradle.kts b/core/stoptime/build.gradle.kts
new file mode 100644
index 0000000..44cf072
--- /dev/null
+++ b/core/stoptime/build.gradle.kts
@@ -0,0 +1,64 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.kotlinSerialization)
+ alias(libs.plugins.androidMultiplatformLibrary)
+ alias(libs.plugins.ksp)
+}
+
+kotlin {
+ android {
+ namespace = "moe.lava.banksia.core.stoptime"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+ }
+
+ compilerOptions {
+ freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
+ freeCompilerArgs.add("-Xexpect-actual-classes")
+ }
+
+ iosArm64()
+ iosSimulatorArm64()
+
+ jvm()
+
+ sourceSets {
+ val clientMain by creating {
+ dependsOn(commonMain.get())
+ }
+
+ androidMain.get().dependsOn(clientMain)
+ iosArm64Main.get().dependsOn(clientMain)
+ iosSimulatorArm64Main.get().dependsOn(clientMain)
+
+ androidMain.dependencies {
+ implementation(libs.ktor.client.okhttp)
+ }
+ commonMain.dependencies {
+ implementation(libs.okio)
+ implementation(libs.koin.core)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.contentnegotiation)
+ implementation(libs.ktor.serialization.kotlinx.json)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.datetime)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlinx.serialization.protobuf)
+
+ implementation(projects.core)
+ implementation(projects.core.sqld)
+ }
+ iosMain.dependencies {
+ implementation(libs.ktor.client.darwin)
+ }
+ jvmMain.dependencies {
+ implementation(libs.koin.ktor)
+ implementation(libs.ktor.server.core)
+ }
+ }
+}
diff --git a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.client.kt b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.client.kt
new file mode 100644
index 0000000..2f83304
--- /dev/null
+++ b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.client.kt
@@ -0,0 +1,11 @@
+package moe.lava.banksia.core.data
+
+import moe.lava.banksia.core.data.repositories.StopTimeRepository
+import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+
+internal actual val platformModule = module {
+ singleOf(::StopTimeRepository)
+ singleOf(::StopTimeRemoteDataSource)
+}
diff --git a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.client.kt b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.client.kt
new file mode 100644
index 0000000..ecaff8e
--- /dev/null
+++ b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.client.kt
@@ -0,0 +1,19 @@
+package moe.lava.banksia.core.data.repositories
+
+import kotlinx.coroutines.flow.flow
+import kotlinx.datetime.LocalDate
+import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource
+import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource
+
+actual class StopTimeRepository internal constructor(
+ private val local: StopTimeLocalDataSource,
+ private val remote: StopTimeRemoteDataSource,
+) {
+ actual suspend fun getForStop(id: String, date: LocalDate) = flow {
+ emit(local.getAtStop(id, date))
+
+ remote.getAtStop(id, date)
+ .takeIf { it.isNotEmpty() }
+ ?.let { emit(it) }
+ }
+}
diff --git a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt
new file mode 100644
index 0000000..0c38f64
--- /dev/null
+++ b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt
@@ -0,0 +1,26 @@
+package moe.lava.banksia.core.data.sources.stoptime
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.get
+import io.ktor.client.request.parameter
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.todayIn
+import moe.lava.banksia.core.data.dto.ExtendedStopTime
+import moe.lava.banksia.core.endpoints.Endpoint
+import moe.lava.banksia.core.endpoints.stopTimeByStop
+import kotlin.time.Clock
+
+internal class StopTimeRemoteDataSource(
+ private val client: HttpClient,
+) {
+ suspend fun getAtStop(
+ stopId: String,
+ date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()),
+ ): List {
+ return client.get(Endpoint.stopTimeByStop(stopId)) {
+ parameter("date", date)
+ }.body>()
+ }
+}
diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.kt
new file mode 100644
index 0000000..d46affa
--- /dev/null
+++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.kt
@@ -0,0 +1,13 @@
+package moe.lava.banksia.core.data
+
+import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource
+import org.koin.core.module.Module
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+
+internal expect val platformModule: Module;
+
+val stopTimeDataDiModule = module {
+ includes(platformModule)
+ singleOf(::StopTimeLocalDataSource)
+}
diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/dto/ExtendedStopTime.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/dto/ExtendedStopTime.kt
new file mode 100644
index 0000000..38de29d
--- /dev/null
+++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/dto/ExtendedStopTime.kt
@@ -0,0 +1,34 @@
+package moe.lava.banksia.core.data.dto
+
+import kotlinx.datetime.LocalDate
+import kotlinx.serialization.Serializable
+import moe.lava.banksia.core.model.FutureTime
+import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.core.model.TimeType
+import moe.lava.banksia.core.model.atDate
+import moe.lava.banksia.core.sqld.GetExtendedForStop
+
+@Serializable
+data class ExtendedStopTime(
+ val patternId: Long,
+ val stopPlatformCode: String?,
+ val time: TimeType.Dated,
+ val headsign: String?,
+ val routeType: RouteType,
+ val routeNumber: String?,
+ val routeName: String,
+)
+
+// TODO: This probably doesn't belong here
+fun GetExtendedForStop.asModel(date: LocalDate) = ExtendedStopTime(
+ patternId = patternId,
+ stopPlatformCode = stopPlatformCode,
+ time = TimeType.Undated(
+ arrival = FutureTime.fromInt((departureTime + arrivalDelta).toInt()),
+ departure = FutureTime.fromInt(departureTime.toInt()),
+ ).atDate(date),
+ headsign = headsign,
+ routeType = RouteType.from(routeType.toInt()),
+ routeNumber = routeNumber,
+ routeName = routeName,
+)
diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt
new file mode 100644
index 0000000..6a81c09
--- /dev/null
+++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt
@@ -0,0 +1,15 @@
+package moe.lava.banksia.core.data.repositories
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.todayIn
+import moe.lava.banksia.core.data.dto.ExtendedStopTime
+import kotlin.time.Clock
+
+expect class StopTimeRepository {
+ suspend fun getForStop(
+ id: String,
+ date: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()),
+ ): Flow>
+}
diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt
new file mode 100644
index 0000000..f22dc09
--- /dev/null
+++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt
@@ -0,0 +1,30 @@
+package moe.lava.banksia.core.data.sources.stoptime
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import kotlinx.coroutines.withContext
+import kotlinx.datetime.LocalDate
+import moe.lava.banksia.core.data.dto.ExtendedStopTime
+import moe.lava.banksia.core.data.dto.asModel
+import moe.lava.banksia.core.sqld.StopTimeQueries
+import moe.lava.banksia.core.util.serialise
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+
+internal class StopTimeLocalDataSource : KoinComponent {
+ private val queries get() = get()
+
+ suspend fun getAtStop(stopId: String, date: LocalDate): List {
+ return withContext(context = Dispatchers.IO) {
+ queries
+ .getExtendedForStop(
+ listOf(date.dayOfWeek).serialise().toLong(),
+ date.toEpochDays(),
+ stopId,
+ )
+ .executeAsList()
+ .map { it.asModel(date) }
+ .sortedBy { it.time.departure }
+ }
+ }
+}
diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/StopTimeEndpoints.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/StopTimeEndpoints.kt
new file mode 100644
index 0000000..f689b2d
--- /dev/null
+++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/StopTimeEndpoints.kt
@@ -0,0 +1,3 @@
+package moe.lava.banksia.core.endpoints
+
+fun Endpoint.stopTimeByStop(stopId: String) = "stoptimes/by_stop/${stopId}"
diff --git a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.jvm.kt b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.jvm.kt
new file mode 100644
index 0000000..70ef406
--- /dev/null
+++ b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.jvm.kt
@@ -0,0 +1,9 @@
+package moe.lava.banksia.core.data
+
+import moe.lava.banksia.core.data.repositories.StopTimeRepository
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+
+internal actual val platformModule = module {
+ singleOf(::StopTimeRepository)
+}
diff --git a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.jvm.kt b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.jvm.kt
new file mode 100644
index 0000000..b4c37a6
--- /dev/null
+++ b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.jvm.kt
@@ -0,0 +1,13 @@
+package moe.lava.banksia.core.data.repositories
+
+import kotlinx.coroutines.flow.flow
+import kotlinx.datetime.LocalDate
+import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource
+
+actual class StopTimeRepository internal constructor(
+ private val local: StopTimeLocalDataSource,
+) {
+ actual suspend fun getForStop(id: String, date: LocalDate) = flow {
+ emit(local.getAtStop(id, date))
+ }
+}
diff --git a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/server/routes/StopTimeRoute.kt b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/server/routes/StopTimeRoute.kt
new file mode 100644
index 0000000..5791855
--- /dev/null
+++ b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/server/routes/StopTimeRoute.kt
@@ -0,0 +1,27 @@
+package moe.lava.banksia.server.routes
+
+import io.ktor.server.response.respond
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.get
+import kotlinx.coroutines.flow.first
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.todayIn
+import moe.lava.banksia.core.data.repositories.StopTimeRepository
+import moe.lava.banksia.core.endpoints.Endpoint
+import moe.lava.banksia.core.endpoints.stopTimeByStop
+import org.koin.ktor.ext.inject
+import kotlin.time.Clock
+
+fun Route.stopTimeRoutes() {
+ val repo by inject()
+
+ get(Endpoint.stopTimeByStop("{stop_id}")) {
+ val stopId = call.parameters["stop_id"]!!
+ val date = call.queryParameters["date"]
+ ?.let { LocalDate.parse(it, LocalDate.Formats.ISO) }
+ ?: Clock.System.todayIn(TimeZone.currentSystemDefault())
+ val data = repo.getForStop(stopId, date).first()
+ call.respond(data)
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index 80feb8f..f0cf36f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,5 +12,3 @@ android.useAndroidX=true
#Ktor
io.ktor.development=true
-
-kotlin.mpp.enableCInteropCommonization=true
\ No newline at end of file
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 0000000..9b7b12a
--- /dev/null
+++ b/gradle/gradle-daemon-jvm.properties
@@ -0,0 +1,13 @@
+#This file is generated by updateDaemonJvm
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/536afcd1dff540251f85e5d2c80458cf/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/ecd23fd7707c683afbcd6052998cb6a9/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/536afcd1dff540251f85e5d2c80458cf/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/536afcd1dff540251f85e5d2c80458cf/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e55dccbfe27cb97945148c61a39c89c5/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/dbd05c4936d573642f94cd149e1356c8/redirect
+toolchainVendor=JETBRAINS
+toolchainVersion=21
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b54a4eb..483c5d5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,41 +1,36 @@
[versions]
-agp = "8.13.1"
-android-compileSdk = "36"
+agp = "9.1.0"
+android-compileSdk = "37"
android-minSdk = "24"
-android-targetSdk = "36"
-androidx-activityCompose = "1.12.4"
-androidx-appcompat = "1.7.0"
-androidx-constraintlayout = "2.2.1"
-androidx-core-ktx = "1.15.0"
-androidx-espresso-core = "3.6.1"
-androidx-lifecycle = "2.9.6"
-androidx-material = "1.12.0"
-androidx-test-junit = "1.2.1"
-compose-multiplatform = "1.11.0-alpha02"
+android-targetSdk = "37"
+androidx-activity= "1.13.0"
+androidx-lifecycle = "2.10.0"
+compose-multiplatform = "1.12.0-alpha02"
composeunstyled = "1.49.6"
coroutines = "1.10.2"
geo = "0.8.0"
-junit = "4.13.2"
-koin = "4.1.1"
-kotlin = "2.3.10"
+koin = "4.2.0"
+kotlin = "2.3.20"
kotlinxDatetime = "0.7.1"
kotlinxSerializationCsv = "0.2.18"
kotlinxSerialization = "1.10.0"
ksp = "2.3.4"
-ktor = "3.4.0"
+ktor = "3.4.1"
logback = "1.5.32"
maplibre = "0.12.1"
material = "1.7.3"
-material3 = "1.11.0-alpha02"
-okio = "3.16.4"
+material3 = "1.11.0-alpha07"
+okio = "3.17.0"
playServicesLocation = "21.3.0"
-room = "2.8.4"
secretsGradlePlugin = "2.0.1"
-spm = "1.4.9"
-sqlite = "2.6.2"
-wire = "5.5.0"
+sqldelight = "2.3.2"
+wire = "6.1.0"
[libraries]
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
+androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" }
compose-material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "material" }
@@ -45,18 +40,11 @@ compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-mu
compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "compose-multiplatform" }
compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-multiplatform" }
composeunstyled = { module = "com.composables:composeunstyled", version.ref = "composeunstyled" }
-moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" }
-moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" }
-kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
-androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
-androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
-androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
-androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
-koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
+kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
@@ -74,19 +62,19 @@ ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "k
ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre" }
+moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" }
+moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
-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" }
-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
-sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite" }
secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }
+sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
+sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
+sqldelight-driver-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose-multiplatform" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
-androidLibrary = { id = "com.android.library", version.ref = "agp" }
+androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
@@ -94,7 +82,6 @@ kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref =
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
-room = { id = "androidx.room", version.ref = "room" }
secretsGradle = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" }
-spm = { id = "io.github.frankois944.spmForKmp", version.ref = "spm" }
+sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
wire = { id = "com.squareup.wire", version.ref = "wire" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 37f853b..37f78a6 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
index 22d2bc6..412e378 100644
--- a/iosApp/iosApp/Info.plist
+++ b/iosApp/iosApp/Info.plist
@@ -46,7 +46,5 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
- NSLocationWhenInUseUsageDescription
- This permission is required to display your current location
diff --git a/server/build.gradle.kts b/server/build.gradle.kts
index 2f7d989..9d2cb78 100644
--- a/server/build.gradle.kts
+++ b/server/build.gradle.kts
@@ -5,15 +5,27 @@ plugins {
application
}
-group = "moe.lava.banksia"
+group = "moe.lava.banksia.server"
version = "1.0.0"
application {
mainClass.set("moe.lava.banksia.server.ApplicationKt")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}")
}
+kotlin {
+ compilerOptions {
+ freeCompilerArgs.add("-Xexplicit-backing-fields")
+ }
+}
+
dependencies {
- implementation(projects.shared)
+ implementation(projects.core)
+ implementation(projects.core.data)
+ implementation(projects.core.sqld)
+ implementation(projects.core.stoptime)
+ implementation(projects.server.gtfs)
+ implementation(projects.server.gtfsRt)
+
implementation(libs.logback)
implementation(libs.koin.core)
implementation(libs.koin.ktor)
@@ -26,8 +38,6 @@ dependencies {
implementation(libs.ktor.server.contentnegotiation)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
- implementation(libs.room.runtime)
- implementation(libs.sqlite.bundled)
testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit)
}
diff --git a/server/gtfs/build.gradle.kts b/server/gtfs/build.gradle.kts
new file mode 100644
index 0000000..8f6d646
--- /dev/null
+++ b/server/gtfs/build.gradle.kts
@@ -0,0 +1,20 @@
+plugins {
+ alias(libs.plugins.kotlinJvm)
+ alias(libs.plugins.kotlinSerialization)
+}
+
+kotlin {
+ compilerOptions {
+ freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
+ freeCompilerArgs.add("-Xexplicit-backing-fields")
+ }
+}
+
+dependencies {
+ implementation(projects.core)
+ implementation(libs.kotlinx.serialization.csv)
+ implementation(libs.kotlinx.datetime)
+ implementation(libs.ktor.client.contentnegotiation)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.okhttp)
+}
diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt
new file mode 100644
index 0000000..c844499
--- /dev/null
+++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt
@@ -0,0 +1,388 @@
+package moe.lava.banksia.server.gtfs
+
+import com.lightningkite.kotlinx.serialization.csv.CsvFormat
+import com.lightningkite.kotlinx.serialization.csv.StringDeferringConfig
+import io.ktor.client.HttpClient
+import io.ktor.client.request.prepareRequest
+import io.ktor.client.request.url
+import io.ktor.client.statement.bodyAsChannel
+import io.ktor.util.cio.writeChannel
+import io.ktor.util.logging.Logger
+import io.ktor.utils.io.copyAndClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.datetime.DayOfWeek
+import kotlinx.datetime.LocalDate
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.modules.EmptySerializersModule
+import kotlinx.serialization.serializer
+import moe.lava.banksia.core.Constants
+import moe.lava.banksia.core.model.FutureTime.Companion.asInt
+import moe.lava.banksia.core.model.Route
+import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.core.model.Service
+import moe.lava.banksia.core.model.ServiceException
+import moe.lava.banksia.core.model.Shape
+import moe.lava.banksia.core.model.Stop
+import moe.lava.banksia.core.model.StopTime
+import moe.lava.banksia.core.model.StoppingPattern
+import moe.lava.banksia.core.model.TimeType
+import moe.lava.banksia.core.model.Trip
+import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.server.gtfs.structures.GtfsRoute
+import moe.lava.banksia.server.gtfs.structures.GtfsService
+import moe.lava.banksia.server.gtfs.structures.GtfsServiceException
+import moe.lava.banksia.server.gtfs.structures.GtfsShape
+import moe.lava.banksia.server.gtfs.structures.GtfsStop
+import moe.lava.banksia.server.gtfs.structures.GtfsStopTime
+import moe.lava.banksia.server.gtfs.structures.GtfsTrip
+import java.io.File
+import java.nio.ByteBuffer
+import java.security.MessageDigest
+import java.util.zip.ZipFile
+import kotlin.time.ExperimentalTime
+
+private typealias StopWithSource = Pair
+
+sealed class GtfsData {
+ data class RouteChunk(val routes: List) : GtfsData()
+ data class ServiceChunk(val services: List) : GtfsData()
+ data class ServiceExceptionChunk(val exceptions: List) : GtfsData()
+ data class ShapeChunk(val shapes: List) : GtfsData()
+ data class StopChunk(val stops: List) : GtfsData()
+ data class TripChunk(val trips: List) : GtfsData()
+}
+
+class GtfsParser(
+ private val log: Logger,
+ private val client: HttpClient,
+) {
+ private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule()))
+ private val datasetPath = File("/tmp/banksia", "dataset.zip")
+
+ @OptIn(ExperimentalTime::class)
+ suspend fun update(datasetUrl: String): Flow {
+ val parentDir = datasetPath.parentFile
+ @Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions")
+ if (parentDir.exists() && !Constants.devMode)
+ parentDir.deleteRecursively()
+
+ parentDir.mkdirs()
+
+ log.info("fetching..")
+ client.prepareRequest {
+ url(datasetUrl)
+ }.execute { resp ->
+ if (!datasetPath.exists())
+ resp.bodyAsChannel().copyAndClose(datasetPath.writeChannel())
+ log.info("fetched!")
+ }
+
+ log.info("extracting...")
+ @Suppress("KotlinConstantConditions")
+ val files = if (Constants.devMode) {
+ datasetPath.parentFile
+ .listFiles { it.isDirectory }
+ .flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() }
+ .ifEmpty { extractAll(datasetPath) }
+// .filter { it.parentFile.name == "2" }
+ } else {
+ extractAll(datasetPath)
+ }
+
+ log.info("parsing...")
+ return parse(files)
+ .onCompletion {
+ @Suppress("KotlinConstantConditions")
+ if (!Constants.devMode) {
+ parentDir.deleteRecursively()
+ }
+
+ log.info("done!")
+ }
+ }
+
+ private fun parse(files: List) = flow {
+ files
+ .filter { it.name == "routes.txt" }
+ .forEach { emit(GtfsData.RouteChunk(parseRoutes(it))) }
+
+ files
+ .filter { it.name == "stops.txt" }
+ .flatMap { parseStops(it) }
+ .let { emit(GtfsData.StopChunk(fixupDuplicateStops(it))) }
+
+ files
+ .filter { it.name == "shapes.txt" }
+ .forEach { emit(GtfsData.ShapeChunk(parseShapes(it))) }
+
+ val services = files
+ .filter { it.name == "calendar.txt" }
+ .flatMap { fd ->
+ parseServices(fd)
+ .also { emit(GtfsData.ServiceChunk(it)) }
+ }
+ .associateBy { it.id }
+
+ files
+ .filter { it.name == "calendar_dates.txt" }
+ .forEach { emit(GtfsData.ServiceExceptionChunk(parseServiceExceptions(it))) }
+
+ val trips = files
+ .filter { it.name == "trips.txt" }
+ .flatMap { fd ->
+ parseTrips(fd, services)
+ }
+ .associateBy { it.id }
+
+ files
+ .filter { it.name == "stop_times.txt" }
+ .forEach { fd ->
+ log.info("parsing stop times for ${fd.parent}...")
+ parseStopTimes(fd) { seq ->
+ val times = ArrayList>(1000100)
+ seq.forEach { pair ->
+ val (_, stoptime) = pair
+ if (times.size > 1000000 && stoptime.patternId == 1L) {
+ emit(GtfsData.TripChunk(processStoptimes(trips, times)))
+ times.clear()
+ }
+
+ times.add(pair)
+ }
+ emit(GtfsData.TripChunk(processStoptimes(trips, times)))
+ }
+ }
+ }
+
+ private fun hashCalc(headsign: String, stops: List): Long {
+ val inst = MessageDigest.getInstance("SHA-256")
+ inst.update(headsign.toByteArray())
+ stops.forEach {
+ inst.update(it.stopId.toByteArray())
+ val dint = it.time.departure.asInt()
+ inst.update((dint).toByte())
+ inst.update((dint shr 8).toByte())
+ val aint = it.time.arrival.asInt()
+ inst.update((aint).toByte())
+ inst.update((aint shr 8).toByte())
+ }
+
+ val buf = inst.digest().slice(0..7).toByteArray()
+ buf[0] = 0
+ buf[1] = 0
+ return ByteBuffer.wrap(buf).long
+ }
+
+ private fun processStoptimes(trips: Map, times: ArrayList>) =
+ times.groupBy { it.first }
+ .map { (tripId, pairs) ->
+ val trip = trips[tripId]!!
+ val stoptimes = pairs.map { it.second }
+ val hash = hashCalc(trip.pattern.headsign, stoptimes)
+ trip.copy(pattern = trip.pattern.copy(
+ id = hash,
+ stoptimes = stoptimes.map { it.copy(patternId = hash) }
+ ))
+ }
+
+ private fun parseRoutes(fd: File) =
+ fd.parseCsv()
+ .map { with(it) {
+ Route(
+ id = route_id,
+ type = RouteType.from(fd.parentFile.name.toInt()),
+ number = route_short_name,
+ name = route_long_name,
+ )
+ } }
+
+ private fun parseShapes(fd: File) =
+ fd.parseCsv()
+ .groupBy { it.shape_id }
+ .map { (id, group) ->
+ val points = group
+ .sortedBy { it.shape_pt_sequence }
+ .map { Point(it.shape_pt_lat, it.shape_pt_lon) }
+
+ Shape(id, points)
+ }
+
+ private fun parseStops(fd: File): List =
+ fd.parseCsv()
+ .map { with(it) {
+ fd.parentFile.name to Stop(
+ id = stop_id,
+ name = stop_name,
+ pos = Point(stop_lat, stop_lon),
+ parent = parent_station.ifEmpty { null },
+ hasWheelChairBoarding = wheelchair_boarding == "1",
+ level = level_id.ifEmpty { null },
+ platformCode = platform_code.ifEmpty { null },
+ )
+ } }
+
+ private inline fun parseStopTimes(fd: File, block: (Sequence>) -> Unit) =
+ fd.parseCsvSequence { seq ->
+ seq
+ .map { with(it) {
+ it.trip_id to StopTime(
+ patternId = stop_sequence,
+ stopId = stop_id,
+ time = TimeType.Undated(
+ arrival = GtfsStopTime.parseGtfsTime(arrival_time),
+ departure = GtfsStopTime.parseGtfsTime(departure_time),
+ ),
+ pickupType = pickup_type,
+ dropOffType = drop_off_type,
+ )
+ } }
+ .let { block(it) }
+ }
+
+ private fun parseServices(fd: File) =
+ fd.parseCsv()
+ .map { with(it) {
+ val days = buildList {
+ if (monday == 1) add(DayOfWeek.MONDAY)
+ if (tuesday == 1) add(DayOfWeek.TUESDAY)
+ if (wednesday == 1) add(DayOfWeek.WEDNESDAY)
+ if (thursday == 1) add(DayOfWeek.THURSDAY)
+ if (friday == 1) add(DayOfWeek.FRIDAY)
+ if (saturday == 1) add(DayOfWeek.SATURDAY)
+ if (sunday == 1) add(DayOfWeek.SUNDAY)
+ }
+ Service(
+ id = "${fd.parentFile.name}_${service_id}",
+ days = days,
+ start = LocalDate.parse(start_date, LocalDate.Formats.ISO_BASIC),
+ end = LocalDate.parse(end_date, LocalDate.Formats.ISO_BASIC),
+ )
+ } }
+
+ private fun parseServiceExceptions(fd: File) =
+ fd.parseCsv()
+ .map { with(it) {
+ ServiceException(
+ serviceId = "${fd.parentFile.name}_${service_id}",
+ date = LocalDate.parse(date, LocalDate.Formats.ISO_BASIC),
+ type = exception_type,
+ )
+ } }
+
+ private fun parseTrips(fd: File, services: Map) =
+ fd.parseCsv()
+ .map { with(it) {
+ Trip.Undated(
+ id = trip_id,
+ pattern = StoppingPattern(
+ id = 0,
+ routeId = route_id,
+ shapeId = shape_id,
+ headsign = trip_headsign,
+ wheelchairAccessible = wheelchair_accessible == "1",
+ stoptimes = listOf()
+ ),
+ service = services["${fd.parentFile.name}_${service_id}"]!!,
+ directionId = direction_id.toInt(),
+ blockId = block_id.ifEmpty { null },
+ )
+ } }
+
+ private fun extract(fd: File): List {
+ val outputs = mutableListOf()
+ ZipFile(fd).use { zip ->
+ for (entry in zip.entries()) {
+ zip.getInputStream(entry).use { input ->
+ val out = File(fd.parentFile, entry.name)
+ out.parentFile.mkdirs()
+ out.outputStream().use { output ->
+ input.copyTo(output)
+ }
+ outputs.add(out)
+ }
+ }
+ }
+ return outputs
+ }
+
+ private fun extractAll(fd: File) = extract(fd).flatMap(::extract)
+
+ private inline fun File.parseCsv(): List = this
+ .readText()
+ .replace("\uFEFF", "") // remove bom
+ .replace("\r\n", "\n") // crlf -> lf
+ .let { csv.decodeFromString(it) }
+
+ private inline fun File.parseCsvSequence(block: (Sequence) -> Unit) = this
+ .bufferedReader()
+ .use { reader ->
+ val iter = object : CharIterator() {
+ var next: Char? = null
+ override fun nextChar(): Char {
+ if (!hasNext()) {
+ throw NoSuchElementException()
+ }
+ val ret = next!!
+ next = null
+ return ret
+ }
+ override fun hasNext(): Boolean {
+ if (next == null) {
+ do {
+ next = null
+ val new = reader.read()
+ if (new != -1) {
+ next = new.toChar()
+ }
+ } while (next == '\uFEFF' || next == '\r')
+ }
+ return next != null
+ }
+ }
+ block(csv.decodeToSequence(iter, csv.serializersModule.serializer()))
+ }
+
+ // Type priority used to resolve duplicates, preferring the first one in the chain
+ private val typePriorityRanking = listOf(
+ RouteType.MetroTrain,
+ RouteType.RegionalTrain,
+ RouteType.MetroTram,
+ RouteType.MetroBus,
+ RouteType.RegionalBus,
+ RouteType.SkyBus,
+ ).map { it.value.toString() }
+
+ @Suppress("LoggingStringTemplateAsArgument") // ?
+ private fun fixupDuplicateStops(stops: List): List {
+ return stops
+ .groupBy { (_, stops) -> stops.id }
+ .map { (id, stops) ->
+ // Just return it if no duplicate
+ if (stops.size == 1) return@map stops[0].second
+
+ // Just return the first one if all the stops' data match
+ if (stops.withIndex().all { (idx, stop) -> idx == 0 || stop.second == stops[idx - 1].second })
+ return@map stops[0].second
+
+ // Find first stop ordered by the types
+ val res = typePriorityRanking
+ .firstNotNullOfOrNull { type ->
+ stops.find { it.first == type }
+ }
+
+ val (_, stop) = if (res == null) {
+ log.warn("Cannot resolve duplicate stop ${id}, using first one")
+ stops.forEach { (type, stop) -> log.warn(" - ($type): $stop") }
+ stops[0]
+ } else {
+ log.debug("Resolving $id for type ${res.first}")
+ stops.forEach { (type, stop) -> log.debug("${if (res.first == type) "*" else " "} - ($type): $stop") }
+ res
+ }
+
+ stop
+ }
+ }
+}
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
similarity index 91%
rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
index c4eabeb..4b1bad9 100644
--- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
+++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
-data class GtfsRoute(
+internal data class GtfsRoute(
val route_id: String,
val agency_id: String,
val route_short_name: String,
diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt
new file mode 100644
index 0000000..1bf9573
--- /dev/null
+++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt
@@ -0,0 +1,18 @@
+package moe.lava.banksia.server.gtfs.structures
+
+import kotlinx.serialization.Serializable
+
+@Suppress("PropertyName")
+@Serializable
+internal data class GtfsService(
+ val service_id: String,
+ val monday: Int,
+ val tuesday: Int,
+ val wednesday: Int,
+ val thursday: Int,
+ val friday: Int,
+ val saturday: Int,
+ val sunday: Int,
+ val start_date: String,
+ val end_date: String,
+)
diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsServiceException.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsServiceException.kt
new file mode 100644
index 0000000..a31aff0
--- /dev/null
+++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsServiceException.kt
@@ -0,0 +1,11 @@
+package moe.lava.banksia.server.gtfs.structures
+
+import kotlinx.serialization.Serializable
+
+@Suppress("PropertyName")
+@Serializable
+internal data class GtfsServiceException(
+ val service_id: String,
+ val date: String,
+ val exception_type: Int,
+)
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
similarity index 90%
rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
index 19cdfb5..32231ab 100644
--- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
+++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
-data class GtfsShape(
+internal data class GtfsShape(
val shape_id: String,
val shape_pt_lat: Double,
val shape_pt_lon: Double,
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
similarity index 92%
rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
index 023a3e1..cb1a018 100644
--- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
+++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
-data class GtfsStop(
+internal data class GtfsStop(
val stop_id: String,
val stop_name: String,
val stop_lat: Double,
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
similarity index 84%
rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
index 61e8a1c..c0bbaf2 100644
--- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
+++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
@@ -1,16 +1,16 @@
package moe.lava.banksia.server.gtfs.structures
import kotlinx.serialization.Serializable
-import moe.lava.banksia.model.FutureTime
+import moe.lava.banksia.core.model.FutureTime
@Suppress("PropertyName")
@Serializable
-data class GtfsStopTime(
+internal data class GtfsStopTime(
val trip_id: String,
val arrival_time: String,
val departure_time: String,
val stop_id: String,
- val stop_sequence: Int,
+ val stop_sequence: Long,
val stop_headsign: String,
val pickup_type: Int,
val drop_off_type: Int,
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
similarity index 92%
rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
index fcfc864..0b0d865 100644
--- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
+++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
-data class GtfsTrip(
+internal data class GtfsTrip(
val route_id: String,
val service_id: String,
val trip_id: String,
diff --git a/server/gtfs_rt/build.gradle.kts b/server/gtfs_rt/build.gradle.kts
new file mode 100644
index 0000000..2887e0b
--- /dev/null
+++ b/server/gtfs_rt/build.gradle.kts
@@ -0,0 +1,32 @@
+plugins {
+ alias(libs.plugins.kotlinJvm)
+ alias(libs.plugins.kotlinSerialization)
+ alias(libs.plugins.wire)
+}
+
+kotlin {
+ compilerOptions {
+ freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
+ freeCompilerArgs.add("-Xexplicit-backing-fields")
+ }
+}
+
+dependencies {
+ implementation(projects.core)
+ implementation(libs.okio)
+ implementation(libs.koin.core)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.contentnegotiation)
+ implementation(libs.ktor.serialization.kotlinx.json)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.datetime)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlinx.serialization.protobuf)
+}
+
+wire {
+ sourcePath {
+ srcDir("src/main/proto")
+ }
+ kotlin {}
+}
diff --git a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsRealtime.kt b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsRealtime.kt
new file mode 100644
index 0000000..128f141
--- /dev/null
+++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsRealtime.kt
@@ -0,0 +1,12 @@
+package moe.lava.banksia.server.gtfsrt
+
+import com.google.transit.realtime.FeedMessage
+
+abstract class GtfsRealtime(protected val data: FeedMessage) {
+ companion object {
+ inline fun parse(ctor: (FeedMessage) -> T, data: ByteArray): T {
+ val message = FeedMessage.ADAPTER.decode(data)
+ return ctor(message)
+ }
+ }
+}
diff --git a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtArchiver.kt b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtArchiver.kt
new file mode 100644
index 0000000..aaee0a9
--- /dev/null
+++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtArchiver.kt
@@ -0,0 +1,116 @@
+package moe.lava.banksia.server.gtfsrt
+
+import com.google.transit.realtime.FeedMessage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import moe.lava.banksia.core.util.log
+import java.io.File
+import kotlin.time.Instant
+
+private const val BASE_DIR = "./data/gtfsr-archive/"
+
+internal class GtfsrtArchiver {
+ private var started = false
+
+ suspend fun start(flow: SharedFlow>) {
+ if (started) {
+ log("GtfsrtArchiver", "Tried to start when already started")
+ return
+ }
+ started = true
+ coroutineScope {
+ launch { compressJob() }
+
+ flow.collect { (type, rawData) ->
+ val data = try {
+ FeedMessage.ADAPTER.decode(rawData)
+ } catch (e: Throwable) {
+ log("gtfsr $type", "Failed to parse proto: $e")
+ return@collect
+ }
+ val timestamp = data.header_.timestamp
+ ?: return@collect log("gtfsr $type", "Failed to read proto timestamp")
+
+ val time = Instant.fromEpochSeconds(timestamp).toLocalDateTime(TimeZone.currentSystemDefault())
+
+ val (prevWeek, prevDay) = (time.dayOfYear - 1) / 7 to (time.dayOfYear - 1) % 7
+ val (nextWeek, nextDay) = time.dayOfYear / 7 to time.dayOfYear % 7
+
+ val base = File(BASE_DIR, type)
+ val previousParent = File(base, "${time.year}-${prevWeek.toString().padStart(2, '0')}/${prevDay}")
+ val currentParent = File(base, "${time.year}-${nextWeek.toString().padStart(2, '0')}/${nextDay}")
+ val target = File(currentParent, "${timestamp}.proto")
+
+ if (previousParent.isDirectory) {
+ enqueueCompression(previousParent)
+ if (prevWeek != nextWeek) {
+ enqueueCompression(previousParent.parentFile)
+ }
+ }
+
+ if (!target.exists()) {
+ try {
+ if (!target.parentFile.isDirectory) {
+ target.parentFile.mkdirs()
+ }
+ target.writeBytes(rawData)
+ } catch (e: Throwable) {
+ log("gtfsr $type", "Failed to write ${target}: $e")
+ }
+ }
+ }
+ }
+ }
+
+ private val cqueue = mutableSetOf()
+ private val ignore = mutableSetOf()
+ private val cmut = Mutex()
+ private suspend fun enqueueCompression(fd: File) {
+ cmut.withLock { cqueue.add(fd) }
+ }
+
+ private suspend fun compressJob() {
+ while(true) {
+ while(true) {
+ val next = cmut.withLock { cqueue.firstOrNull() }
+ ?: break
+ if (!next.isDirectory) {
+ cmut.withLock { cqueue.remove(next) }
+ continue
+ }
+ if (next in ignore) continue
+
+ withContext(Dispatchers.IO) {
+ val proc = ProcessBuilder(
+ "tar", "-acf",
+ "${next.absolutePath}.tar.zst",
+ next.absolutePath
+ ).start()
+ val exitCode = proc.waitFor()
+ if (exitCode == 0) {
+ log("CompressJob", "Compressed ${next.absolutePath} to ${next.absolutePath}.tar.zst")
+ if (next.deleteRecursively()) {
+ cmut.withLock { cqueue.remove(next) }
+ } else {
+ log("CompressJob", "Failed to delete $next")
+ ignore.add(next)
+ }
+ } else {
+ val msg = proc.errorStream.readAllBytes().decodeToString()
+ log("CompressJob", "Failed to delete $next (exit code $exitCode")
+ log("CompressJob", msg)
+ }
+ }
+ }
+ delay(30000)
+ }
+ }
+}
diff --git a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtService.kt b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtService.kt
new file mode 100644
index 0000000..6f46ed7
--- /dev/null
+++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtService.kt
@@ -0,0 +1,87 @@
+package moe.lava.banksia.server.gtfsrt
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import io.ktor.client.request.header
+import io.ktor.client.request.url
+import io.ktor.client.statement.bodyAsText
+import io.ktor.client.statement.readRawBytes
+import io.ktor.http.isSuccess
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+import moe.lava.banksia.core.Constants
+import moe.lava.banksia.core.util.LogScope
+import moe.lava.banksia.core.util.log
+
+private val types = arrayOf(
+ "metro/trip-updates",
+ "metro/vehicle-positions",
+ "metro/service-alerts",
+ "tram/trip-updates",
+ "tram/vehicle-positions",
+ "tram/service-alerts",
+ "bus/trip-updates",
+ "bus/vehicle-positions",
+ "vline/trip-updates",
+ "vline/vehicle-positions",
+)
+
+class GtfsrtService(
+ private val client: HttpClient,
+) {
+ private val archiver = GtfsrtArchiver()
+ private var started = false
+
+ internal val rawMessages: SharedFlow>
+ field = MutableSharedFlow>()
+
+ fun start(
+ scope: CoroutineScope,
+ enableArchiving: Boolean = false,
+ ) {
+ if (started) {
+ log("GtfsrtService", "Tried to start when already started")
+ return
+ }
+
+ if (enableArchiving) {
+ scope.launch { archiver.start(rawMessages) }
+ }
+
+ scope.launch { fetch() }
+ }
+
+ private suspend fun fetch() {
+ coroutineScope {
+ types.map { type ->
+ launch(context = Dispatchers.IO) {
+ val logger = LogScope("gtfsr $type")
+ try {
+ val res = client.get {
+ url("https://api.opendata.transport.vic.gov.au/opendata/public-transport/gtfs/realtime/v1/${type}")
+ header("KeyId", Constants.opendataKey)
+ }
+ if (!res.status.isSuccess()) {
+ logger.log("${res.status} | ${res.bodyAsText()}")
+ } else {
+ val bytes = res.readRawBytes()
+ rawMessages.emit(type to bytes)
+ }
+ } catch (e: Throwable) {
+ logger.log("$e")
+ logger.log(e.stackTraceToString())
+ }
+ }
+ }.joinAll()
+ }
+
+ delay(10000)
+ fetch()
+ }
+}
diff --git a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/RealtimeVehiclePositions.kt b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/RealtimeVehiclePositions.kt
new file mode 100644
index 0000000..4466b91
--- /dev/null
+++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/RealtimeVehiclePositions.kt
@@ -0,0 +1,22 @@
+package moe.lava.banksia.server.gtfsrt
+
+import com.google.transit.realtime.FeedMessage
+import moe.lava.banksia.core.util.Point
+
+class RealtimeVehiclePositions(data: FeedMessage) : GtfsRealtime(data) {
+ private val positions = mutableMapOf()
+
+ init {
+ data.entity
+ .mapNotNull { ent ->
+ if (ent.vehicle?.position == null) return@mapNotNull null
+ ent.id to ent.vehicle.position.run {
+ Point(latitude.toDouble(), longitude.toDouble())
+ }
+ }
+ .let { positions.putAll(it) }
+ }
+
+ fun getAll() = positions.toMap()
+ fun forTrip(tripId: String) = positions[tripId]
+}
diff --git a/shared/src/commonMain/proto/gtfs-realtime.proto b/server/gtfs_rt/src/main/proto/gtfs-realtime.proto
similarity index 100%
rename from shared/src/commonMain/proto/gtfs-realtime.proto
rename to server/gtfs_rt/src/main/proto/gtfs-realtime.proto
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt
index 4ae3398..dedffe5 100644
--- a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt
+++ b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt
@@ -15,37 +15,59 @@ import io.ktor.server.routing.routing
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import moe.lava.banksia.Constants
-import moe.lava.banksia.di.CommonModules
-import moe.lava.banksia.room.dao.RouteDao
-import moe.lava.banksia.room.dao.StopDao
-import moe.lava.banksia.room.dao.VersionMetadataDao
+import moe.lava.banksia.core.Constants
+import moe.lava.banksia.core.sqld.RouteQueries
+import moe.lava.banksia.core.sqld.StopQueries
+import moe.lava.banksia.core.sqld.mappers.asModel
import moe.lava.banksia.server.di.ServerModules
-import moe.lava.banksia.server.gtfs.GtfsHandler
-import moe.lava.banksia.server.gtfsr.GtfsrService
+import moe.lava.banksia.server.gtfsrt.GtfsrtService
+import moe.lava.banksia.server.routes.stopTimeRoutes
import org.koin.dsl.module
-import org.koin.ktor.ext.inject
+import org.koin.ktor.ext.get
import org.koin.ktor.plugin.Koin
fun main() {
+ if (System.getenv("BANKSIA_PRODUCTION") == "1") Constants.devMode = false
+
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
+ log.info("devMode: ${Constants.devMode}")
install(ContentNegotiation) {
json()
}
install(Koin) {
modules(module { single { log } })
- modules(CommonModules, ServerModules)
+ modules(ServerModules)
}
- val gtfsr by inject()
- launch { gtfsr.start() }
+ @Suppress("KotlinConstantConditions")
+ launch { get().start(this, !Constants.devMode) }
routing {
- get("/update") {
+ stopTimeRoutes()
+
+ if (Constants.devMode) {
+ get("/fixup") {
+ call.respondText("received")
+ get().addParentsToStops()
+ }
+ }
+ get("/manage/fixup") {
+ val key = call.parameters["key"]
+ if (key != Constants.updateKey) {
+ call.respond(HttpStatusCode.Forbidden)
+ return@get
+ }
+
+ call.respondText("fixing")
+ launch(context = Dispatchers.IO) {
+ get().addParentsToStops()
+ }
+ }
+ get("/manage/update") {
val key = call.parameters["key"]
if (key != Constants.updateKey) {
call.respond(HttpStatusCode.Forbidden)
@@ -57,30 +79,14 @@ fun Application.module() {
?: "https://opendata.transport.vic.gov.au/dataset/3f4e292e-7f8a-4ffe-831f-1953be0fe448/resource/${datasetUuid}/download/gtfs.zip"
call.respondText("received")
launch(context = Dispatchers.IO) {
- val handler by inject()
- handler.update(datasetUrl)
- }
- }
-
- get("/metadata/{type?}") {
- val dao by inject()
- val type = call.parameters["type"]
- if (type == null) {
- call.respond(dao.getAll().map { it.asModel() })
- return@get
- }
-
- val data = dao.get(type)?.asModel()
- if (data == null) {
- call.respond(HttpStatusCode.NotFound)
- } else {
- call.respond(data)
+ get().import(datasetUrl)
+ get().addParentsToStops()
}
}
get("/routes") {
val routes = withContext(context = Dispatchers.IO) {
- inject().value.getAll()
+ get().getAll().executeAsList()
}
val res = routes.map { it.asModel() }
call.respond(res)
@@ -88,16 +94,17 @@ fun Application.module() {
get("/routes/{route_id}") {
val routeId = call.parameters["route_id"]!!
val route = withContext(context = Dispatchers.IO) {
- inject().value.get(routeId)
+ get().get(routeId).executeAsOneOrNull()
}
- if (route != null)
+ if (route != null) {
call.respond(route.asModel())
- else
+ } else {
call.respond(HttpStatusCode.NotFound)
+ }
}
get("/stops") {
val routes = withContext(context = Dispatchers.IO) {
- inject().value.getAll()
+ get().getAll().executeAsList()
}
val res = routes.map { it.asModel() }
call.respond(res)
@@ -105,38 +112,26 @@ fun Application.module() {
get("/stops/{stop_id}") {
val stopId = call.parameters["stop_id"]!!
val stop = withContext(context = Dispatchers.IO) {
- inject().value.get(stopId)
+ get().get(stopId).executeAsOneOrNull()
}
- if (stop != null)
+ if (stop != null) {
call.respond(stop.asModel())
- else
+ } else {
call.respond(HttpStatusCode.NotFound)
+ }
}
get("/route_stops/{route_id}") {
val routeId = call.parameters["route_id"]!!
- val useParent = call.queryParameters["parent"] in listOf("true", "1")
+ val useParent = call.queryParameters["parent"] !in listOf("false", "0")
val stops = withContext(Dispatchers.IO) {
- val routeDao by inject()
- if (useParent)
- routeDao.stopsParent(routeId)
- else
- routeDao.stops(routeId)
+ val queries = get()
+ if (useParent) {
+ queries.getParentsByRoute(routeId).executeAsList()
+ } else {
+ queries.getByRoute(routeId).executeAsList()
+ }
}
call.respond(stops.map { it.asModel() })
-// val stops = withContext(Dispatchers.IO) {
-// val stopDao by inject()
-// val stopTimeDao by inject()
-// val tripDao by inject()
-//
-// tripDao.getByRoute(routeId)
-// .map { it.id }
-// .let { stopTimeDao.get(it) }
-// .flatMap { it.asModel().stopInfos }
-// .map { it.stopId }
-// .let { stopDao.get(it) }
-// .map { it.asModel() }
-// }
-// call.respond(stops)
}
}
}
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt b/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt
new file mode 100644
index 0000000..97892e0
--- /dev/null
+++ b/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt
@@ -0,0 +1,45 @@
+package moe.lava.banksia.server
+
+import moe.lava.banksia.core.sqld.BanksiaDatabase
+import moe.lava.banksia.core.util.log
+import java.security.MessageDigest
+import moe.lava.banksia.core.sqld.Stop as DbStop
+
+class GtfsDataFixer(
+ private val database: BanksiaDatabase,
+) {
+ fun addParentsToStops() {
+ val queries = database.stopQueries
+ val stops = queries.getAllParentless().executeAsList()
+ stops
+ .groupBy { it.name.split("/")[0] }
+ .filter { (_, stops) -> stops.size > 1 }
+ .forEach { (name, stops) ->
+ val avgLat = stops.map { it.lat }.average()
+ val avgLng = stops.map { it.lng }.average()
+ val hash = name.sha256().substring(0, 7)
+ val parentId = "bsia:df1:$hash"
+ val parent = DbStop(
+ id = parentId,
+ name = name,
+ lat = avgLat,
+ lng = avgLng,
+ parent = null,
+ hasWheelChairBoarding = if (stops.all { it.hasWheelChairBoarding == 1L }) 1L else 0L,
+ level = "",
+ platformCode = "",
+ )
+ log("datafixer", "inserting ${parentId} for ${stops.size} children")
+ queries.transaction {
+ queries.insert(parent)
+ queries.updateParents(parentId, stops.map { it.id })
+ }
+ }
+ }
+}
+
+private fun String.sha256() =
+ MessageDigest
+ .getInstance("SHA-256")
+ .digest(this.toByteArray())
+ .joinToString("") { "%02x".format(it) }
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt b/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt
new file mode 100644
index 0000000..84fae70
--- /dev/null
+++ b/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt
@@ -0,0 +1,115 @@
+package moe.lava.banksia.server
+
+import io.ktor.util.logging.Logger
+import moe.lava.banksia.core.model.Route
+import moe.lava.banksia.core.model.Service
+import moe.lava.banksia.core.model.ServiceException
+import moe.lava.banksia.core.model.Shape
+import moe.lava.banksia.core.model.Stop
+import moe.lava.banksia.core.model.Trip
+import moe.lava.banksia.core.sqld.DatabaseManager
+import moe.lava.banksia.core.sqld.mappers.asDb
+import moe.lava.banksia.server.gtfs.GtfsData
+import moe.lava.banksia.server.gtfs.GtfsParser
+import kotlin.time.Clock
+import moe.lava.banksia.core.sqld.BanksiaDatabase as Database
+
+class GtfsImporter(
+ private val parser: GtfsParser,
+ private val dbm: DatabaseManager,
+ private val log: Logger,
+) {
+ suspend fun import(url: String, date: Long = Clock.System.now().epochSeconds) {
+ val (database, close) = dbm.makeAlt()
+
+ parser.update(url).collect { chunk ->
+ when (chunk) {
+ is GtfsData.RouteChunk -> database.addRoutes(chunk.routes)
+ is GtfsData.ServiceChunk -> database.addServices(chunk.services)
+ is GtfsData.ServiceExceptionChunk -> database.addServiceExceptions(chunk.exceptions)
+ is GtfsData.ShapeChunk -> database.addShapes(chunk.shapes)
+ is GtfsData.StopChunk -> database.addStops(chunk.stops)
+ is GtfsData.TripChunk -> database.addTrips(chunk.trips)
+ }
+ }
+
+ close()
+ dbm.swap()
+ }
+
+ private fun Database.addRoutes(routes: List) {
+ log.info("inserting routes...")
+ routeQueries.transaction {
+ routes.forEach {
+ routeQueries.insert(it.asDb())
+ }
+ }
+ log.info("done")
+ }
+
+ private fun Database.addServices(services: List) {
+ log.info("inserting services...")
+ serviceQueries.transaction {
+ services.forEach {
+ serviceQueries.insert(it.asDb())
+ }
+ }
+ log.info("done")
+ }
+
+ private fun Database.addServiceExceptions(exceptions: List) {
+ log.info("inserting exceptions...")
+ serviceExceptionQueries.transaction {
+ exceptions.forEach {
+ serviceExceptionQueries.insert(it.asDb())
+ }
+ }
+ log.info("done")
+ }
+
+ private fun Database.addShapes(shapes: List) {
+ log.info("inserting shapes...")
+ shapeQueries.transaction {
+ shapes.forEach {
+ shapeQueries.insert(it.asDb())
+ }
+ }
+ log.info("done")
+ }
+
+ private fun Database.addStops(stops: List) {
+ log.info("inserting stops...")
+ stops
+ .groupBy { it.id }
+ .forEach { (id, gstops) ->
+ if (gstops.size > 1) {
+ if (gstops.withIndex().any { (i, stop) -> i != 0 && stop != gstops[i - 1] }) {
+ gstops.forEach {
+ log.warn("duplicate $id: $it")
+ }
+ }
+ }
+ }
+
+ stopQueries.transaction {
+ stops.forEach {
+ stopQueries.insert(it.asDb())
+ }
+ }
+ log.info("done")
+ }
+
+ private fun Database.addTrips(trips: List) {
+ log.info("inserting ${trips.size} trips...")
+ transaction {
+ trips.forEach { trip ->
+ stoppingPatternQueries.insert(trip.pattern.asDb())
+ trip.pattern.stoptimes.forEach { stoptime ->
+ stopTimeQueries.insert(stoptime.asDb())
+ }
+ tripQueries.insert(trip.asDb())
+ }
+ }
+ log.info("done")
+ }
+}
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt b/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt
index c7b650c..b2593b3 100644
--- a/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt
+++ b/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt
@@ -1,13 +1,22 @@
package moe.lava.banksia.server.di
import io.ktor.client.HttpClient
-import moe.lava.banksia.server.gtfs.GtfsHandler
-import moe.lava.banksia.server.gtfsr.GtfsrService
+import moe.lava.banksia.core.data.dataDiModule
+import moe.lava.banksia.server.GtfsDataFixer
+import moe.lava.banksia.server.GtfsImporter
+import moe.lava.banksia.server.gtfs.GtfsParser
+import moe.lava.banksia.server.gtfsrt.GtfsrtService
+import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val ServerModules = module {
+ includes(dataDiModule)
+
single { HttpClient() }
- singleOf(::GtfsHandler)
- singleOf(::GtfsrService)
+ singleOf(::GtfsParser)
+ singleOf(::GtfsrtService)
+
+ factoryOf(::GtfsDataFixer)
+ factoryOf(::GtfsImporter)
}
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt
deleted file mode 100644
index d85d5df..0000000
--- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt
+++ /dev/null
@@ -1,294 +0,0 @@
-package moe.lava.banksia.server.gtfs
-
-import com.lightningkite.kotlinx.serialization.csv.CsvFormat
-import com.lightningkite.kotlinx.serialization.csv.StringDeferringConfig
-import io.ktor.client.HttpClient
-import io.ktor.client.request.prepareRequest
-import io.ktor.client.request.url
-import io.ktor.client.statement.bodyAsChannel
-import io.ktor.util.cio.writeChannel
-import io.ktor.util.logging.Logger
-import io.ktor.utils.io.copyAndClose
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.modules.EmptySerializersModule
-import kotlinx.serialization.serializer
-import moe.lava.banksia.Constants
-import moe.lava.banksia.model.Route
-import moe.lava.banksia.model.Shape
-import moe.lava.banksia.model.Stop
-import moe.lava.banksia.model.StopTime
-import moe.lava.banksia.model.Trip
-import moe.lava.banksia.room.Database
-import moe.lava.banksia.room.converter.RouteTypeConverter
-import moe.lava.banksia.room.entity.asEntity
-import moe.lava.banksia.server.gtfs.structures.GtfsRoute
-import moe.lava.banksia.server.gtfs.structures.GtfsShape
-import moe.lava.banksia.server.gtfs.structures.GtfsStop
-import moe.lava.banksia.server.gtfs.structures.GtfsStopTime
-import moe.lava.banksia.server.gtfs.structures.GtfsTrip
-import moe.lava.banksia.util.Point
-import java.io.File
-import java.util.zip.ZipFile
-import kotlin.time.Clock
-import kotlin.time.ExperimentalTime
-
-class GtfsHandler(
- private val log: Logger,
- private val client: HttpClient,
- private val db: Database,
-) {
- private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule()))
- private val datasetPath = File("/tmp/banksia", "dataset.zip")
-
- @OptIn(ExperimentalTime::class)
- suspend fun update(datasetUrl: String, date: Long? = null) {
- val parentDir = datasetPath.parentFile
- @Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions")
- if (parentDir.exists() && !Constants.devMode)
- parentDir.deleteRecursively()
-
- parentDir.mkdirs()
-
- log.info("fetching..")
- client.prepareRequest {
- url(datasetUrl)
- }.execute { resp ->
- if (!datasetPath.exists())
- resp.bodyAsChannel().copyAndClose(datasetPath.writeChannel())
- log.info("fetched!")
- }
-
- log.info("extracting...")
- @Suppress("KotlinConstantConditions")
- val files = if (Constants.devMode) {
- datasetPath.parentFile
- .listFiles { it.isDirectory }
- .flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() }
- .ifEmpty { extractAll(datasetPath) }
- } else {
- extractAll(datasetPath)
- }
-
- addRoutes(files)
- addStops(files)
- addShapes(files)
- addTrips(files)
- addStopTimes(files)
-
- updateMetadata(date ?: Clock.System.now().epochSeconds)
-
- @Suppress("KotlinConstantConditions")
- if (!Constants.devMode) {
- parentDir.deleteRecursively()
- }
-
- log.info("done!")
- }
-
- private suspend fun updateMetadata(date: Long) {
- val dao = db.versionMetadataDao
- log.info("updating metadata...")
- dao.update(date, listOf("routes", "stops", "shapes", "trips", "stop_times"))
- }
-
- private suspend fun addRoutes(files: List) {
- val dao = db.routeDao
- log.info("parsing routes...")
- val routes = files
- .filter { it.name == "routes.txt" }
- .flatMap { fd -> parseRoutes(fd) }
-
- log.info("inserting routes...")
- dao.deleteAll()
- dao.insertAll(*routes.map { it.asEntity() }.toTypedArray())
- }
-
- private fun parseRoutes(fd: File) =
- fd.parseCsv()
- .map { with(it) {
- Route(
- id = route_id,
- type = RouteTypeConverter.from(fd.parentFile.name.toInt()),
- number = route_short_name,
- name = route_long_name,
- )
- } }
-
- private suspend fun addShapes(files: List) {
- val dao = db.shapeDao
- log.info("parsing shapes...")
- val shapes = files
- .filter { it.name == "shapes.txt" }
- .flatMap { fd -> parseShapes(fd) }
-
- log.info("inserting shapes...")
- dao.deleteAll()
- dao.insertAll(*shapes.map { it.asEntity() }.toTypedArray())
- }
-
- private fun parseShapes(fd: File) =
- fd.parseCsv()
- .groupBy { it.shape_id }
- .map { (id, group) ->
- val points = group
- .sortedBy { it.shape_pt_sequence }
- .map { Point(it.shape_pt_lat, it.shape_pt_lon) }
-
- Shape(id, points)
- }
-
- private suspend fun addStops(files: List) {
- val dao = db.stopDao
- log.info("parsing stops...")
- val stops = files
- .filter { it.name == "stops.txt" }
- .flatMap { fd -> parseStops(fd) }
-
- log.info("inserting stops...")
- dao.deleteAll()
- stops
- .groupBy { it.id }
- .forEach { (id, gstops) ->
- if (gstops.size > 1) {
- if (gstops.withIndex().any { (i, stop) -> i != 0 && stop != gstops[i - 1] }) {
- gstops.forEach {
- log.info("duplicate $id: $it")
- }
- }
- }
- }
- dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray())
- }
-
- private fun parseStops(fd: File) =
- fd.parseCsv()
- .map { with(it) {
- Stop(
- id = stop_id,
- name = stop_name,
- pos = Point(stop_lat, stop_lon),
- parent = parent_station,
- hasWheelChairBoarding = wheelchair_boarding == "1",
- level = level_id,
- platformCode = platform_code,
- )
- } }
-
- private suspend fun addStopTimes(files: List) {
- val dao = db.stopTimeDao
- dao.deleteAll()
- log.info("parsing stop times...")
- files
- .filter { it.name == "stop_times.txt" }
- .forEach { fd ->
- log.info("parsing stop times for ${fd.parent}...")
- parseStopTimes(fd) { seq ->
- seq.chunked(1000000)
- .forEach { queue ->
- log.info("converting stop times (${queue.size}) for ${fd.parent}...")
- val conv = queue.map { it.asEntity() }.toTypedArray()
- log.info("inserting stop times (${conv.size}) for ${fd.parent}...")
- dao.insertOrReplaceAll(*conv)
- }
- }
- }
- }
-
- private inline fun parseStopTimes(fd: File, block: (Sequence) -> Unit) =
- fd.parseCsvSequence { seq ->
- seq
- .map { with(it) {
- StopTime(
- tripId = trip_id,
- stopId = stop_id,
- arrivalTime = GtfsStopTime.parseGtfsTime(arrival_time),
- departureTime = GtfsStopTime.parseGtfsTime(departure_time),
- headsign = stop_headsign,
- pickupType = pickup_type,
- dropOffType = drop_off_type,
- )
- } }
- .let { block(it) }
- }
-
- private suspend fun addTrips(files: List) {
- val dao = db.tripDao
- log.info("parsing trips...")
- val trips = files
- .filter { it.name == "trips.txt" }
- .flatMap { fd -> parseTrips(fd) }
-
- log.info("inserting trips...")
- dao.deleteAll()
- dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray())
- }
-
- private fun parseTrips(fd: File) =
- fd.parseCsv()
- .map { with(it) {
- Trip(
- id = trip_id,
- routeId = route_id,
- serviceId = service_id,
- shapeId = shape_id.ifEmpty { null },
- tripHeadsign = trip_headsign,
- directionId = direction_id,
- blockId = block_id,
- wheelchairAccessible = wheelchair_accessible,
- )
- } }
-
- private fun extract(fd: File): List {
- val outputs = mutableListOf()
- ZipFile(fd).use { zip ->
- for (entry in zip.entries()) {
- zip.getInputStream(entry).use { input ->
- val out = File(fd.parentFile, entry.name)
- out.parentFile.mkdirs()
- out.outputStream().use { output ->
- input.copyTo(output)
- }
- outputs.add(out)
- }
- }
- }
- return outputs
- }
-
- private fun extractAll(fd: File) = extract(fd).flatMap(::extract)
-
- private inline fun File.parseCsv(): List = this
- .readText()
- .replace("\uFEFF", "") // remove bom
- .replace("\r\n", "\n") // crlf -> lf
- .let { csv.decodeFromString(it) }
-
- private inline fun File.parseCsvSequence(block: (Sequence) -> Unit) = this
- .bufferedReader()
- .use { reader ->
- val iter = object : CharIterator() {
- var next: Char? = null
- override fun nextChar(): Char {
- if (!hasNext()) {
- throw NoSuchElementException()
- }
- val ret = next!!
- next = null
- return ret
- }
- override fun hasNext(): Boolean {
- if (next == null) {
- do {
- next = null
- val new = reader.read()
- if (new != -1) {
- next = new.toChar()
- }
- } while (next == '\uFEFF' || next == '\r')
- }
- return next != null
- }
- }
- block(csv.decodeToSequence(iter, csv.serializersModule.serializer()))
- }
-}
diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfsr/GtfsrService.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfsr/GtfsrService.kt
deleted file mode 100644
index 5a0b1dc..0000000
--- a/server/src/main/kotlin/moe/lava/banksia/server/gtfsr/GtfsrService.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-package moe.lava.banksia.server.gtfsr
-
-import com.google.transit.realtime.FeedMessage
-import io.ktor.client.HttpClient
-import io.ktor.client.request.get
-import io.ktor.client.request.header
-import io.ktor.client.request.url
-import io.ktor.client.statement.bodyAsText
-import io.ktor.client.statement.readRawBytes
-import io.ktor.http.isSuccess
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.joinAll
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withContext
-import moe.lava.banksia.Constants
-import moe.lava.banksia.util.LogScope
-import moe.lava.banksia.util.log
-import java.io.File
-import java.time.Instant
-import java.time.ZoneId
-
-private const val BASE_DIR = "./data/gtfsr-archive/"
-
-class GtfsrService(private val client: HttpClient) {
- private var started = false
- private val latest = mutableMapOf()
-
- fun latestFor(type: String) = latest[type]
-
- private val iFlow = MutableSharedFlow>()
- val flow = iFlow.asSharedFlow()
-
- companion object {
- val types = arrayOf(
- "metro/trip-updates",
- "metro/vehicle-positions",
- "metro/service-alerts",
- "tram/trip-updates",
- "tram/vehicle-positions",
- "tram/service-alerts",
- "bus/trip-updates",
- "bus/vehicle-positions",
- "vline/trip-updates",
- "vline/vehicle-positions",
- )
- }
-
- suspend fun start() {
- if (started) {
- log("GtfsrService", "Tried to start when already started")
- return
- }
- started = true
- coroutineScope {
- launch { compressJob() }
-
- while (true) {
- val results = mutableMapOf()
- types.map { type ->
- launch(context = Dispatchers.IO) {
- val logger = LogScope("gtfsr $type")
- try {
- val res = client.get {
- url("https://api.opendata.transport.vic.gov.au/opendata/public-transport/gtfs/realtime/v1/${type}")
- header("KeyId", Constants.opendataKey)
- }
- if (!res.status.isSuccess()) {
- logger.log("${res.status} | ${res.bodyAsText()}")
- } else {
- results[type] = res.readRawBytes()
- }
- } catch (e: Throwable) {
- logger.log("$e")
- logger.log(e.stackTraceToString())
- }
- }
- }.joinAll()
-
- results.forEach { (type, data) ->
- val dec = try {
- FeedMessage.ADAPTER.decode(data)
- } catch (e: Throwable) {
- log("gtfsr $type", "Failed to parse proto: $e")
- return@forEach
- }
- val timestamp = dec.header_.timestamp
- ?: return@forEach log("gtfsr $type", "Failed to read proto timestamp")
-
- val time = Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
-
- val base = File(BASE_DIR, type)
- val previousParent = File(base, "${time.year}-${((time.dayOfYear - 1) / 7).toString().padStart(2, '0')}")
- val currentParent = File(base, "${time.year}-${((time.dayOfYear - 1) / 7 + 1).toString().padStart(2, '0')}")
- val target = File(currentParent, "${timestamp}.proto")
-
- if (previousParent.isDirectory) {
- enqueueCompression(previousParent)
- }
-
- if (!target.exists()) {
- try {
- if (!target.parentFile.isDirectory) {
- target.parentFile.mkdirs()
- }
- target.writeBytes(data)
- } catch (e: Throwable) {
- log("gtfsr $type", "Failed to write ${target}: $e")
- }
- }
- }
- delay(10000)
- }
- }
- }
-
- private val cqueue = mutableSetOf()
- private val ignore = mutableSetOf()
- private val cmut = Mutex()
- private suspend fun enqueueCompression(fd: File) {
- cmut.withLock { cqueue.add(fd) }
- }
-
- private suspend fun compressJob() {
- while(true) {
- while(true) {
- val next = cmut.withLock { cqueue.firstOrNull() }
- ?: break
- if (!next.isDirectory) {
- cmut.withLock { cqueue.remove(next) }
- continue
- }
- if (next in ignore) continue
-
- withContext(Dispatchers.IO) {
- val proc = ProcessBuilder(
- "tar", "-acf",
- "${next.absolutePath}.tar.zst",
- next.absolutePath
- ).start()
- val exitCode = proc.waitFor()
- if (exitCode == 0) {
- if (next.deleteRecursively()) {
- cmut.withLock { cqueue.remove(next) }
- } else {
- log("CompressJob", "Failed to delete $next")
- ignore.add(next)
- }
- } else {
- val msg = proc.errorStream.readAllBytes().decodeToString()
- log("CompressJob", "Failed to delete $next (exit code $exitCode")
- log("CompressJob", msg)
- }
- }
- }
- delay(30000)
- }
- }
-}
diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml
index de5d8bf..6519371 100644
--- a/server/src/main/resources/logback.xml
+++ b/server/src/main/resources/logback.xml
@@ -14,7 +14,7 @@
%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
-
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3649a7a..bdb499e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -14,6 +14,9 @@ pluginManagement {
gradlePluginPortal()
}
}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
dependencyResolutionManagement {
repositories {
@@ -28,6 +31,14 @@ dependencyResolutionManagement {
}
}
-include(":composeApp")
+include(":androidApp")
include(":server")
-include(":shared")
+include(":server:gtfs")
+include(":server:gtfs_rt")
+include(":core")
+include(":core:data")
+include(":core:stoptime")
+include(":core:sqld")
+include(":ui")
+include(":ui:maps")
+include(":ui:shared")
diff --git a/shared/schemas/moe.lava.banksia.room.Database/1.json b/shared/schemas/moe.lava.banksia.room.Database/1.json
deleted file mode 100644
index 037062e..0000000
--- a/shared/schemas/moe.lava.banksia.room.Database/1.json
+++ /dev/null
@@ -1,72 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 1,
- "identityHash": "e536f5a9b1408377bcc449195169648c",
- "entities": [
- {
- "tableName": "Route",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "type",
- "columnName": "type",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "number",
- "columnName": "number",
- "affinity": "TEXT"
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- }
- },
- {
- "tableName": "Shape",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "path",
- "columnName": "path",
- "affinity": "BLOB",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- }
- }
- ],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e536f5a9b1408377bcc449195169648c')"
- ]
- }
-}
\ No newline at end of file
diff --git a/shared/schemas/moe.lava.banksia.room.Database/2.json b/shared/schemas/moe.lava.banksia.room.Database/2.json
deleted file mode 100644
index 04a14e3..0000000
--- a/shared/schemas/moe.lava.banksia.room.Database/2.json
+++ /dev/null
@@ -1,315 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 2,
- "identityHash": "83ece554400bb035c267dc2414c23293",
- "entities": [
- {
- "tableName": "Route",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "type",
- "columnName": "type",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "number",
- "columnName": "number",
- "affinity": "TEXT"
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- }
- },
- {
- "tableName": "Shape",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "path",
- "columnName": "path",
- "affinity": "BLOB",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- }
- },
- {
- "tableName": "Stop",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "lat",
- "columnName": "lat",
- "affinity": "REAL",
- "notNull": true
- },
- {
- "fieldPath": "lng",
- "columnName": "lng",
- "affinity": "REAL",
- "notNull": true
- },
- {
- "fieldPath": "parent",
- "columnName": "parent",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "hasWheelChairBoarding",
- "columnName": "hasWheelChairBoarding",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "level",
- "columnName": "level",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "platformCode",
- "columnName": "platformCode",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- },
- "indices": [
- {
- "name": "index_Stop_parent",
- "unique": false,
- "columnNames": [
- "parent"
- ],
- "orders": [],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)"
- }
- ]
- },
- {
- "tableName": "StopTime",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "tripId",
- "columnName": "tripId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "stopId",
- "columnName": "stopId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "arrivalTime",
- "columnName": "arrivalTime",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "departureTime",
- "columnName": "departureTime",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "headsign",
- "columnName": "headsign",
- "affinity": "TEXT"
- },
- {
- "fieldPath": "pickupType",
- "columnName": "pickupType",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "dropOffType",
- "columnName": "dropOffType",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "tripId",
- "stopId"
- ]
- },
- "foreignKeys": [
- {
- "table": "Trip",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "tripId"
- ],
- "referencedColumns": [
- "id"
- ]
- },
- {
- "table": "Stop",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "stopId"
- ],
- "referencedColumns": [
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "Trip",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "routeId",
- "columnName": "routeId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "serviceId",
- "columnName": "serviceId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shapeId",
- "columnName": "shapeId",
- "affinity": "TEXT"
- },
- {
- "fieldPath": "tripHeadsign",
- "columnName": "tripHeadsign",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "directionId",
- "columnName": "directionId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "blockId",
- "columnName": "blockId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "wheelchairAccessible",
- "columnName": "wheelchairAccessible",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- },
- "indices": [
- {
- "name": "index_Trip_routeId",
- "unique": false,
- "columnNames": [
- "routeId"
- ],
- "orders": [],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "Route",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "routeId"
- ],
- "referencedColumns": [
- "id"
- ]
- },
- {
- "table": "Shape",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "shapeId"
- ],
- "referencedColumns": [
- "id"
- ]
- }
- ]
- }
- ],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '83ece554400bb035c267dc2414c23293')"
- ]
- }
-}
\ No newline at end of file
diff --git a/shared/schemas/moe.lava.banksia.room.Database/3.json b/shared/schemas/moe.lava.banksia.room.Database/3.json
deleted file mode 100644
index e769926..0000000
--- a/shared/schemas/moe.lava.banksia.room.Database/3.json
+++ /dev/null
@@ -1,339 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 3,
- "identityHash": "5a7252ab3bcae4d0d0950024b19ba002",
- "entities": [
- {
- "tableName": "Route",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "type",
- "columnName": "type",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "number",
- "columnName": "number",
- "affinity": "TEXT"
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- }
- },
- {
- "tableName": "Shape",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "path",
- "columnName": "path",
- "affinity": "BLOB",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- }
- },
- {
- "tableName": "Stop",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "lat",
- "columnName": "lat",
- "affinity": "REAL",
- "notNull": true
- },
- {
- "fieldPath": "lng",
- "columnName": "lng",
- "affinity": "REAL",
- "notNull": true
- },
- {
- "fieldPath": "parent",
- "columnName": "parent",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "hasWheelChairBoarding",
- "columnName": "hasWheelChairBoarding",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "level",
- "columnName": "level",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "platformCode",
- "columnName": "platformCode",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- },
- "indices": [
- {
- "name": "index_Stop_parent",
- "unique": false,
- "columnNames": [
- "parent"
- ],
- "orders": [],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)"
- }
- ]
- },
- {
- "tableName": "StopTime",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "tripId",
- "columnName": "tripId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "stopId",
- "columnName": "stopId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "arrivalTime",
- "columnName": "arrivalTime",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "departureTime",
- "columnName": "departureTime",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "headsign",
- "columnName": "headsign",
- "affinity": "TEXT"
- },
- {
- "fieldPath": "pickupType",
- "columnName": "pickupType",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "dropOffType",
- "columnName": "dropOffType",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "tripId",
- "stopId"
- ]
- },
- "foreignKeys": [
- {
- "table": "Trip",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "tripId"
- ],
- "referencedColumns": [
- "id"
- ]
- },
- {
- "table": "Stop",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "stopId"
- ],
- "referencedColumns": [
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "Trip",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "routeId",
- "columnName": "routeId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "serviceId",
- "columnName": "serviceId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shapeId",
- "columnName": "shapeId",
- "affinity": "TEXT"
- },
- {
- "fieldPath": "tripHeadsign",
- "columnName": "tripHeadsign",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "directionId",
- "columnName": "directionId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "blockId",
- "columnName": "blockId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "wheelchairAccessible",
- "columnName": "wheelchairAccessible",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- },
- "indices": [
- {
- "name": "index_Trip_routeId",
- "unique": false,
- "columnNames": [
- "routeId"
- ],
- "orders": [],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "Route",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "routeId"
- ],
- "referencedColumns": [
- "id"
- ]
- },
- {
- "table": "Shape",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "shapeId"
- ],
- "referencedColumns": [
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "VersionMetadata",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))",
- "fields": [
- {
- "fieldPath": "type",
- "columnName": "type",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "lastUpdated",
- "columnName": "lastUpdated",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "type"
- ]
- }
- }
- ],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5a7252ab3bcae4d0d0950024b19ba002')"
- ]
- }
-}
\ No newline at end of file
diff --git a/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt b/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt
deleted file mode 100644
index 0447f4b..0000000
--- a/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package moe.lava.banksia.di
-
-import android.content.Context
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import moe.lava.banksia.room.Database
-import org.koin.core.parameter.ParametersHolder
-import org.koin.core.scope.Scope
-import org.koin.dsl.module
-
-class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
- override fun getBuilder(): RoomDatabase.Builder {
- val appContext = ctx.applicationContext
- val dbFile = appContext.getDatabasePath("room.db")
- return Room.databaseBuilder(
- context = appContext,
- name = dbFile.absolutePath
- )
- }
-}
-
-actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
- AndroidDatabaseBuilder(get())
-
-internal actual val ExtPlatformModule = module { }
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt
deleted file mode 100644
index 823174b..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package moe.lava.banksia.di
-
-import moe.lava.banksia.room.Database
-import org.koin.dsl.module
-
-val CommonModules = module {
- includes(PlatformModule)
-
- single { Database.build(get().getBuilder()) }
- single { get().versionMetadataDao }
- single { get().routeDao }
- single { get().shapeDao }
- single { get().stopDao }
- single { get().stopTimeDao }
- single { get().tripDao }
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt
deleted file mode 100644
index 6f29f14..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package moe.lava.banksia.di
-
-import androidx.room.RoomDatabase
-import moe.lava.banksia.room.Database
-import org.koin.core.module.Module
-import org.koin.core.parameter.ParametersHolder
-import org.koin.core.scope.Scope
-import org.koin.dsl.module
-
-interface PlatformDatabaseBuilder {
- fun getBuilder(): RoomDatabase.Builder
-}
-
-expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder
-
-internal expect val ExtPlatformModule: Module
-
-internal val PlatformModule = module {
- includes(ExtPlatformModule)
- single { provideDatabaseBuilder(it) }
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt
deleted file mode 100644
index 682839d..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package moe.lava.banksia.model
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class StopTime(
- val tripId: String,
- val stopId: String,
- val arrivalTime: FutureTime,
- val departureTime: FutureTime,
- val headsign: String?,
- val pickupType: Int,
- val dropOffType: Int,
-)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt
deleted file mode 100644
index ef95eea..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package moe.lava.banksia.model
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class Trip(
- val id: String,
- val routeId: String,
- val serviceId: String,
- val shapeId: String?,
- val tripHeadsign: String,
- val directionId: String,
- val blockId: String,
- val wheelchairAccessible: String,
-)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt
deleted file mode 100644
index 0a5024d..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package moe.lava.banksia.room
-
-import androidx.room.AutoMigration
-import androidx.room.ConstructedBy
-import androidx.room.RoomDatabase
-import androidx.room.RoomDatabaseConstructor
-import androidx.room.TypeConverters
-import androidx.sqlite.driver.bundled.BundledSQLiteDriver
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.IO
-import moe.lava.banksia.room.converter.RouteTypeConverter
-import moe.lava.banksia.room.dao.VersionMetadataDao
-import moe.lava.banksia.room.dao.RouteDao
-import moe.lava.banksia.room.dao.ShapeDao
-import moe.lava.banksia.room.dao.StopDao
-import moe.lava.banksia.room.dao.StopTimeDao
-import moe.lava.banksia.room.dao.TripDao
-import moe.lava.banksia.room.entity.RouteEntity
-import moe.lava.banksia.room.entity.ShapeEntity
-import moe.lava.banksia.room.entity.StopEntity
-import moe.lava.banksia.room.entity.StopTimeEntity
-import moe.lava.banksia.room.entity.TripEntity
-import moe.lava.banksia.room.entity.VersionMetadataEntity
-import androidx.room.Database as DatabaseAnnotation
-
-@DatabaseAnnotation(
- version = 3,
- entities = [
- RouteEntity::class,
- ShapeEntity::class,
- StopEntity::class,
- StopTimeEntity::class,
- TripEntity::class,
- VersionMetadataEntity::class,
- ],
- autoMigrations = [
- AutoMigration(from = 1, to = 2),
- AutoMigration(from = 2, to = 3),
- ]
-)
-@TypeConverters(RouteTypeConverter::class)
-@ConstructedBy(DatabaseConstructor::class)
-abstract class Database : RoomDatabase() {
- abstract val versionMetadataDao: VersionMetadataDao
- abstract val routeDao: RouteDao
- abstract val shapeDao: ShapeDao
- abstract val stopDao: StopDao
- abstract val stopTimeDao: StopTimeDao
- abstract val tripDao: TripDao
-
- companion object {
- fun build(base: Builder) =
- base.fallbackToDestructiveMigration(true)
- .setDriver(BundledSQLiteDriver())
- .setQueryCoroutineContext(Dispatchers.IO)
-// .fallbackToDestructiveMigration(true)
- .build()
- }
-}
-
-@Suppress("KotlinNoActualForExpect")
-expect object DatabaseConstructor : RoomDatabaseConstructor {
- override fun initialize(): Database
-}
\ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt
deleted file mode 100644
index 8927f14..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package moe.lava.banksia.room.converter
-
-import androidx.room.TypeConverter
-import moe.lava.banksia.model.RouteType
-
-object RouteTypeConverter {
- @TypeConverter
- fun from(value: Int) = RouteType.entries.first { it.value == value }
-
- @TypeConverter
- fun to(routeType: RouteType) = routeType.value
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapePathConverter.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapePathConverter.kt
deleted file mode 100644
index 08a8064..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapePathConverter.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package moe.lava.banksia.room.converter
-
-import androidx.room.TypeConverter
-import moe.lava.banksia.model.ShapePath
-import moe.lava.banksia.util.Point
-
-object ShapePathConverter {
- @TypeConverter
- fun from(value: ByteArray): ShapePath {
- return value
- .asIterable()
- .chunked(8) {
- (it[0].toLong() and 0xFF) or
- (it[1].toLong() and 0xFF shl 8) or
- (it[2].toLong() and 0xFF shl 16) or
- (it[3].toLong() and 0xFF shl 24) or
- (it[4].toLong() and 0xFF shl 32) or
- (it[5].toLong() and 0xFF shl 40) or
- (it[6].toLong() and 0xFF shl 48) or
- (it[7].toLong() and 0xFF shl 56)
- }
- .map { Double.fromBits(it) }
- .chunked(2)
- .map { (lat, lng) -> Point(lat, lng) }
- }
-
- @TypeConverter
- fun to(path: ShapePath): ByteArray {
- return path
- .flatMap { (lat, lng) -> listOf(lat.toBits(), lng.toBits()) }
- .flatMap { i -> listOf(
- i.toByte(),
- (i shr 8).toByte(),
- (i shr 16).toByte(),
- (i shr 24).toByte(),
- (i shr 32).toByte(),
- (i shr 40).toByte(),
- (i shr 48).toByte(),
- (i shr 56).toByte(),
- ) }
- .toByteArray()
- }
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt
deleted file mode 100644
index 0174f0f..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package moe.lava.banksia.room.dao
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.Companion.REPLACE
-import androidx.room.Query
-import moe.lava.banksia.room.entity.RouteEntity
-import moe.lava.banksia.room.entity.StopEntity
-
-@Dao
-interface RouteDao {
- @Query("SELECT * FROM Route")
- suspend fun getAll(): List
-
- @Query("SELECT * FROM Route WHERE id == :id")
- suspend fun get(id: String): RouteEntity?
-
- @Insert
- suspend fun insertAll(vararg routes: RouteEntity)
-
- @Insert(onConflict = REPLACE)
- suspend fun insertOrReplaceAll(vararg routes: RouteEntity)
-
- @Delete
- suspend fun delete(route: RouteEntity)
-
- @Query("DELETE FROM Route")
- suspend fun deleteAll()
-
- @Query("""
- SELECT Stop.* FROM Stop
- INNER JOIN StopTime ON StopTime.stopId == Stop.id
- INNER JOIN Trip ON Trip.id == StopTime.tripId
- WHERE Trip.routeId == :id
- GROUP BY Stop.id
- """)
- suspend fun stops(id: String): List
-
- @Query("""
- SELECT Stop.* FROM Stop
- INNER JOIN Stop Child ON Child.parent == Stop.id
- INNER JOIN StopTime ON StopTime.stopId == Child.id
- INNER JOIN Trip ON Trip.id == StopTime.tripId
- WHERE Trip.routeId == :id
- GROUP BY Stop.id
- """)
- suspend fun stopsParent(id: String): List
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt
deleted file mode 100644
index c48735a..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package moe.lava.banksia.room.dao
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.Query
-import moe.lava.banksia.room.entity.ShapeEntity
-
-@Dao
-interface ShapeDao {
- @Query("SELECT * FROM Shape WHERE id == :id")
- suspend fun get(id: String): ShapeEntity?
-
- @Insert
- suspend fun insertAll(vararg shapes: ShapeEntity)
-
- @Delete
- suspend fun delete(shape: ShapeEntity)
-
- @Query("DELETE FROM Shape")
- suspend fun deleteAll()
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt
deleted file mode 100644
index f6b2ef2..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package moe.lava.banksia.room.dao
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.Companion.REPLACE
-import androidx.room.Query
-import moe.lava.banksia.room.entity.StopEntity
-
-@Dao
-interface StopDao {
- @Query("SELECT * FROM Stop")
- suspend fun getAll(): List
-
- @Query("SELECT * FROM Stop WHERE id == :id")
- suspend fun get(id: String): StopEntity?
-
- @Query("SELECT * FROM Stop WHERE id IN (:ids)")
- suspend fun get(ids: List): List
-
- @Insert
- suspend fun insertAll(vararg stops: StopEntity)
-
- @Insert(onConflict = REPLACE)
- suspend fun insertOrReplaceAll(vararg stops: StopEntity)
-
- @Delete
- suspend fun delete(stop: StopEntity)
-
- @Query("DELETE FROM Stop")
- suspend fun deleteAll()
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt
deleted file mode 100644
index 88485f4..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package moe.lava.banksia.room.dao
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.Companion.REPLACE
-import androidx.room.Query
-import moe.lava.banksia.room.entity.StopTimeEntity
-
-@Dao
-interface StopTimeDao {
- @Query("SELECT * FROM StopTime")
- suspend fun getAll(): List
-
- @Query("SELECT * FROM StopTime WHERE tripId == :tripId")
- suspend fun get(tripId: String): StopTimeEntity?
-
- @Query("SELECT * FROM StopTime WHERE tripId IN (:tripIds)")
- suspend fun get(tripIds: List): List
-
- @Insert
- suspend fun insertAll(vararg stopTimes: StopTimeEntity)
-
- @Insert(onConflict = REPLACE)
- suspend fun insertOrReplaceAll(vararg stopTimes: StopTimeEntity)
-
- @Delete
- suspend fun delete(stopTime: StopTimeEntity)
-
- @Query("DELETE FROM StopTime")
- suspend fun deleteAll()
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/TripDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/TripDao.kt
deleted file mode 100644
index 9778a1a..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/TripDao.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package moe.lava.banksia.room.dao
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.Companion.REPLACE
-import androidx.room.Query
-import moe.lava.banksia.room.entity.TripEntity
-
-@Dao
-interface TripDao {
- @Query("SELECT * FROM Trip")
- suspend fun getAll(): List
-
- @Query("SELECT * FROM Trip WHERE id == :id")
- suspend fun get(id: String): TripEntity?
-
- @Query("SELECT * FROM Trip WHERE routeId == :id")
- suspend fun getByRoute(id: String): List
-
- @Insert
- suspend fun insertAll(vararg trips: TripEntity)
-
- @Insert(onConflict = REPLACE)
- suspend fun insertOrReplaceAll(vararg trips: TripEntity)
-
- @Delete
- suspend fun delete(trip: TripEntity)
-
- @Query("DELETE FROM Trip")
- suspend fun deleteAll()
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/VersionMetadataDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/VersionMetadataDao.kt
deleted file mode 100644
index b96102e..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/VersionMetadataDao.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package moe.lava.banksia.room.dao
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.Companion.REPLACE
-import androidx.room.Query
-import moe.lava.banksia.room.entity.VersionMetadataEntity
-
-@Dao
-interface VersionMetadataDao {
- @Query("SELECT * FROM VersionMetadata WHERE type == :type")
- suspend fun get(type: String): VersionMetadataEntity?
-
- @Query("SELECT * FROM VersionMetadata")
- suspend fun getAll(): List
-
- @Insert(onConflict = REPLACE)
- suspend fun update(vararg data: VersionMetadataEntity)
-
- suspend fun update(vararg data: Pair) {
- update(*data.map { (type, lastUpdated) -> VersionMetadataEntity(type, lastUpdated) }.toTypedArray())
- }
-
- suspend fun update(lastUpdated: Long, types: Collection) {
- update(*types.map { VersionMetadataEntity(it, lastUpdated) }.toTypedArray())
- }
-}
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/RouteEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/RouteEntity.kt
deleted file mode 100644
index cc690d6..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/RouteEntity.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package moe.lava.banksia.room.entity
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import moe.lava.banksia.model.Route
-import moe.lava.banksia.model.RouteType
-
-@Entity("Route")
-data class RouteEntity(
- @PrimaryKey val id: String,
- val type: RouteType,
- val number: String?,
- val name: String,
-) {
- fun asModel() = Route(id, type, number, name)
-}
-
-fun Route.asEntity() = RouteEntity(id, type, number, name)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt
deleted file mode 100644
index 4b14a95..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package moe.lava.banksia.room.entity
-
-import kotlinx.datetime.DayOfWeek
-import kotlinx.datetime.LocalDate
-import moe.lava.banksia.model.Service
-
-data class ServiceEntity(
- val id: String,
- val days: Int,
- val start: Int,
- val end: Int,
-) {
- object Parser {
- private fun Int.check(other: Int) = (this and other) != 0
-
- fun deserialiseDays(days: Int): List = buildList {
- if (days.check(1))
- add(DayOfWeek.MONDAY)
- if (days.check(1 shl 1))
- add(DayOfWeek.TUESDAY)
- if (days.check(1 shl 2))
- add(DayOfWeek.WEDNESDAY)
- if (days.check(1 shl 3))
- add(DayOfWeek.THURSDAY)
- if (days.check(1 shl 4))
- add(DayOfWeek.FRIDAY)
- if (days.check(1 shl 5))
- add(DayOfWeek.SATURDAY)
- if (days.check(1 shl 6))
- add(DayOfWeek.SUNDAY)
- }
- fun serialiseDays(days: List): Int =
- days.fold(0) { vl, n ->
- vl + when (n) {
- DayOfWeek.MONDAY -> 1
- DayOfWeek.TUESDAY -> 1 shl 1
- DayOfWeek.WEDNESDAY -> 1 shl 2
- DayOfWeek.THURSDAY -> 1 shl 3
- DayOfWeek.FRIDAY -> 1 shl 4
- DayOfWeek.SATURDAY -> 1 shl 5
- DayOfWeek.SUNDAY -> 1 shl 6
- }
- }
- }
- fun asModel() = Service(
- id,
- Parser.deserialiseDays(days),
- LocalDate.fromEpochDays(start),
- LocalDate.fromEpochDays(end),
- )
-}
-
-fun Service.asEntity() = ServiceEntity(
- id,
- ServiceEntity.Parser.serialiseDays(days),
- start.toEpochDays().toInt(),
- end.toEpochDays().toInt(),
-)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ShapeEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ShapeEntity.kt
deleted file mode 100644
index 87ca671..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ShapeEntity.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package moe.lava.banksia.room.entity
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import androidx.room.TypeConverters
-import moe.lava.banksia.model.Shape
-import moe.lava.banksia.model.ShapePath
-import moe.lava.banksia.room.converter.ShapePathConverter
-
-@Entity("Shape")
-@TypeConverters(ShapePathConverter::class)
-data class ShapeEntity(
- @PrimaryKey val id: String,
- val path: ShapePath,
-) {
- fun asModel() = Shape(id, path)
-}
-
-fun Shape.asEntity() = ShapeEntity(id, path)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt
deleted file mode 100644
index 9c6cf15..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package moe.lava.banksia.room.entity
-
-import androidx.room.ColumnInfo
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import moe.lava.banksia.model.Stop
-import moe.lava.banksia.util.Point
-
-@Entity("Stop")
-data class StopEntity(
- @PrimaryKey val id: String,
- val name: String,
- val lat: Double,
- val lng: Double,
- @ColumnInfo(index = true) val parent: String,
- val hasWheelChairBoarding: Boolean,
- val level: String,
- val platformCode: String,
-) {
- fun asModel() = Stop(id, name, Point(lat, lng), parent, hasWheelChairBoarding, level, platformCode)
-}
-
-fun Stop.asEntity() = StopEntity(id, name, pos.lat, pos.lng, parent, hasWheelChairBoarding, level, platformCode)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt
deleted file mode 100644
index 9b0aac8..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package moe.lava.banksia.room.entity
-
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.ForeignKey.Companion.CASCADE
-import kotlinx.serialization.ExperimentalSerializationApi
-import moe.lava.banksia.model.FutureTime
-import moe.lava.banksia.model.FutureTime.Companion.asInt
-import moe.lava.banksia.model.StopTime
-
-@Entity(
- "StopTime",
- primaryKeys = ["tripId", "stopId"],
- foreignKeys = [
- ForeignKey(TripEntity::class, parentColumns = ["id"], childColumns = ["tripId"], onDelete = CASCADE),
- ForeignKey(StopEntity::class, parentColumns = ["id"], childColumns = ["stopId"], onDelete = CASCADE),
- ]
-)
-data class StopTimeEntity(
- val tripId: String,
- val stopId: String,
- val arrivalTime: Int,
- val departureTime: Int,
- val headsign: String?,
- val pickupType: Int,
- val dropOffType: Int,
-) {
- fun asModel() = StopTime(
- tripId,
- stopId,
- FutureTime.fromInt(arrivalTime),
- FutureTime.fromInt(departureTime),
- headsign,
- pickupType,
- dropOffType,
- )
-}
-
-@OptIn(ExperimentalSerializationApi::class)
-fun StopTime.asEntity() = StopTimeEntity(
- tripId,
- stopId,
- arrivalTime.asInt(),
- departureTime.asInt(),
- headsign,
- pickupType,
- dropOffType,
-)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt
deleted file mode 100644
index ca7e9a7..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package moe.lava.banksia.room.entity
-
-import androidx.room.ColumnInfo
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.ForeignKey.Companion.CASCADE
-import androidx.room.PrimaryKey
-import moe.lava.banksia.model.Trip
-
-@Entity(
- "Trip",
- foreignKeys = [
- ForeignKey(RouteEntity::class, parentColumns = ["id"], childColumns = ["routeId"], onDelete = CASCADE),
- ForeignKey(ShapeEntity::class, parentColumns = ["id"], childColumns = ["shapeId"], onDelete = CASCADE),
- ],
-)
-data class TripEntity(
- @PrimaryKey val id: String,
- @ColumnInfo(index = true) val routeId: String,
- val serviceId: String,
- val shapeId: String?,
- val tripHeadsign: String,
- val directionId: String,
- val blockId: String,
- val wheelchairAccessible: String,
-) {
- fun asModel() = Trip(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible)
-}
-
-fun Trip.asEntity() = TripEntity(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible)
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/VersionMetadataEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/VersionMetadataEntity.kt
deleted file mode 100644
index fc00b44..0000000
--- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/VersionMetadataEntity.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package moe.lava.banksia.room.entity
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import moe.lava.banksia.model.VersionMetadata
-
-@Entity(
- "VersionMetadata",
-)
-data class VersionMetadataEntity(
- /** Entity type this metadata applies to */
- @PrimaryKey val type: String,
- /** Last updated */
- val lastUpdated: Long,
-) {
- fun asModel() = VersionMetadata(type, lastUpdated)
-}
-
-fun VersionMetadata.asEntity() = VersionMetadataEntity(type, lastUpdated)
diff --git a/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt b/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt
deleted file mode 100644
index d5f83a2..0000000
--- a/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package moe.lava.banksia.di
-
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import kotlinx.cinterop.ExperimentalForeignApi
-import moe.lava.banksia.room.Database
-import org.koin.core.parameter.ParametersHolder
-import org.koin.core.scope.Scope
-import org.koin.dsl.module
-import platform.Foundation.NSDocumentDirectory
-import platform.Foundation.NSFileManager
-import platform.Foundation.NSUserDomainMask
-
-class IosDatabaseBuilder() : PlatformDatabaseBuilder {
- @OptIn(ExperimentalForeignApi::class)
- override fun getBuilder(): RoomDatabase.Builder {
- val path = NSFileManager.defaultManager.URLForDirectory(
- directory = NSDocumentDirectory,
- inDomain = NSUserDomainMask,
- appropriateForURL = null,
- create = false,
- error = null,
- )
- val dbPath = path!!.path + "/room.db"
- return Room.databaseBuilder(
- name = dbPath
- )
- }
-}
-
-actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
- IosDatabaseBuilder()
-
-internal actual val ExtPlatformModule = module { }
diff --git a/shared/src/iosMain/kotlin/moe/lava/banksia/util/Logging.ios.kt b/shared/src/iosMain/kotlin/moe/lava/banksia/util/Logging.ios.kt
deleted file mode 100644
index c24034d..0000000
--- a/shared/src/iosMain/kotlin/moe/lava/banksia/util/Logging.ios.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package moe.lava.banksia.util
-
-import platform.Foundation.NSLog
-
-// TODO: use better logging functions maybe(?)
-actual fun log(tag: String, msg: String) {
- NSLog("$tag: $msg")
-}
-
-actual fun error(tag: String, msg: String, throwable: Throwable?) {
- NSLog("$tag: $msg: ${throwable?.stackTraceToString()}")
-}
diff --git a/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt b/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt
deleted file mode 100644
index 3e93241..0000000
--- a/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package moe.lava.banksia.di
-
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import moe.lava.banksia.room.Database
-import org.koin.core.parameter.ParametersHolder
-import org.koin.core.scope.Scope
-import org.koin.dsl.module
-import java.io.File
-
-class JvmDatabaseBuilder() : PlatformDatabaseBuilder {
- override fun getBuilder(): RoomDatabase.Builder {
- val dbFile = File("./data/room.db")
- return Room.databaseBuilder(
- name = dbFile.absolutePath,
- )
- }
-}
-
-actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
- JvmDatabaseBuilder()
-
-internal actual val ExtPlatformModule = module { }
diff --git a/composeApp/build.gradle.kts b/ui/build.gradle.kts
similarity index 55%
rename from composeApp/build.gradle.kts
rename to ui/build.gradle.kts
index 68df64c..b599bc6 100644
--- a/composeApp/build.gradle.kts
+++ b/ui/build.gradle.kts
@@ -1,27 +1,31 @@
-import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-import java.net.URI
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
- alias(libs.plugins.androidApplication)
+ alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.secretsGradle)
- alias(libs.plugins.spm)
}
kotlin {
- androidTarget {
- @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ android {
+ namespace = "moe.lava.banksia.ui"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
+
+ androidResources {
+ enable = true
+ }
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
+ freeCompilerArgs.add("-Xexplicit-backing-fields")
}
listOf(
@@ -29,26 +33,17 @@ kotlin {
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
- iosTarget.compilations {
- getByName("main") {
- cinterops.create("spmMaplibre")
- }
- }
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
-// iosTarget.swiftPackageConfig(cinteropName = "banksia") {
-// }
}
sourceSets {
-
androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
- implementation(libs.androidx.activity.compose)
- implementation(libs.kotlinx.coroutines.android)
implementation(libs.play.services.location)
+ implementation(projects.ui.shared)
}
commonMain.dependencies {
implementation(libs.compose.components.resources)
@@ -72,61 +67,21 @@ kotlin {
implementation(libs.maplibre.compose)
implementation(libs.moko.geo)
implementation(libs.moko.geo.compose)
- implementation(projects.shared)
implementation(libs.ui.backhandler)
- }
- }
-}
-android {
- namespace = "moe.lava.banksia"
- compileSdk = libs.versions.android.compileSdk.get().toInt()
-
- defaultConfig {
- applicationId = "moe.lava.banksia"
- minSdk = libs.versions.android.minSdk.get().toInt()
- targetSdk = libs.versions.android.targetSdk.get().toInt()
- versionCode = 1
- versionName = "1.0"
- }
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ implementation(projects.core)
+ implementation(projects.core.data)
+ implementation(projects.core.stoptime)
+ implementation(projects.ui.maps)
+ implementation(projects.ui.shared)
}
}
- buildTypes {
- getByName("release") {
- isMinifyEnabled = false
- signingConfig = signingConfigs.getByName("debug")
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
}
dependencies {
- debugImplementation(compose.uiTooling)
+ androidRuntimeClasspath(libs.compose.ui.tooling)
}
secrets {
propertiesFileName = "secrets.properties"
}
-
-compose.resources {
- publicResClass = true
- packageOfResClass = "moe.lava.banksia.resources"
-}
-
-swiftPackageConfig {
- create("spmMaplibre") {
- dependency {
- remotePackageVersion(
- url = URI("https://github.com/maplibre/maplibre-gl-native-distribution.git"),
- products = { add("MapLibre") },
- version = "6.17.1",
- )
- }
- }
-}
diff --git a/ui/maps/build.gradle.kts b/ui/maps/build.gradle.kts
new file mode 100644
index 0000000..4e859d1
--- /dev/null
+++ b/ui/maps/build.gradle.kts
@@ -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.core)
+ implementation(projects.ui.shared)
+ }
+ }
+}
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt
new file mode 100644
index 0000000..d76c1f4
--- /dev/null
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt
@@ -0,0 +1,100 @@
+package moe.lava.banksia.ui.map
+
+import androidx.compose.foundation.isSystemInDarkTheme
+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.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonObject
+import moe.lava.banksia.core.Constants
+import moe.lava.banksia.ui.map.mappers.routeColorExpression
+import moe.lava.banksia.ui.map.mappers.toMapPosition
+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
+import kotlin.time.Duration.Companion.seconds
+
+@Composable
+internal fun MapLibreMaps(
+ modifier: Modifier,
+ insets: WindowInsets,
+ positionState: MapsPositionState,
+ stops: GeoJsonData.Features?,
+// vehicles: GeoJsonData.Features?,
+ stopInnerColor: Color = BanksiaTheme.colors.surface,
+ onStopClicked: (Feature) -> Unit,
+) {
+ val camPos = rememberCameraState(
+ CameraPosition(
+ zoom = 16.0,
+ target = MELBOURNE_POS
+ )
+ )
+ val scope = rememberCoroutineScope()
+ scope.launch {
+ positionState.updates.collect {
+ val (position, box) = it.toMapPosition()
+ if (box != null) {
+ camPos.animateTo(box, duration = 1.seconds)
+ } else {
+ camPos.animateTo(position, duration = 1.seconds)
+ }
+ }
+ }
+
+ val variant = if (isSystemInDarkTheme()) "dark" else "light"
+
+ MaplibreMap(
+ modifier = modifier,
+ baseStyle = BaseStyle.Uri("https://api.protomaps.com/styles/v5/$variant/en.json?key=${Constants.protomapsKey}"),
+ 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(stopInnerColor),
+ 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(feature.properties!!)
+ onStopClicked(features[0])
+ ClickResult.Consume
+ }
+ )
+ }
+ }
+}
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt
new file mode 100644
index 0000000..92a9695
--- /dev/null
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt
@@ -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.core.util.Point
+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
+
+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 = listOf(),
+// vehicles: List = 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 = {},
+ )
+}
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt
new file mode 100644
index 0000000..94421a7
--- /dev/null
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt
@@ -0,0 +1,29 @@
+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.ui.map.util.CameraPosition
+
+class MapsPositionState internal constructor(
+ private val scope: CoroutineScope
+) {
+ internal val updates: SharedFlow
+ field = MutableSharedFlow()
+
+ fun update(position: CameraPosition) {
+ scope.launch {
+ updates.emit(position)
+ }
+ }
+}
+
+@Composable
+fun rememberMapsPositionState(): MapsPositionState {
+ val scope = rememberCoroutineScope()
+ return remember { MapsPositionState(scope) }
+}
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/CameraPosition.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/CameraPosition.kt
new file mode 100644
index 0000000..b463c18
--- /dev/null
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/CameraPosition.kt
@@ -0,0 +1,15 @@
+package moe.lava.banksia.ui.map.mappers
+
+import moe.lava.banksia.ui.map.util.CameraPosition
+import org.maplibre.spatialk.geojson.BoundingBox
+import org.maplibre.compose.camera.CameraPosition as MLCameraPosition
+
+internal fun CameraPosition.toMapPosition() = Pair(
+ MLCameraPosition(target = this.centre.toPosition(), zoom = 16.0),
+ this.bounds?.let {
+ BoundingBox(
+ southwest = it.southwest.toPosition(),
+ northeast = it.northeast.toPosition(),
+ )
+ }
+)
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt
new file mode 100644
index 0000000..3fe99c2
--- /dev/null
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt
@@ -0,0 +1,40 @@
+package moe.lava.banksia.ui.map.mappers
+
+import kotlinx.serialization.Serializable
+import moe.lava.banksia.core.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.asFeatures() = GeoJsonData.Features(asFeatureCollection())
+
+internal fun Iterable.asFeatureCollection(): FeatureCollection {
+ 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)
+ }
+ }
+ }
+}
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt
new file mode 100644
index 0000000..ed568c2
--- /dev/null
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt
@@ -0,0 +1,6 @@
+package moe.lava.banksia.ui.map.mappers
+
+import moe.lava.banksia.core.util.Point
+import org.maplibre.spatialk.geojson.Position
+
+internal fun Point.toPosition() = Position(lng, lat)
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt
new file mode 100644
index 0000000..584c76f
--- /dev/null
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt
@@ -0,0 +1,19 @@
+package moe.lava.banksia.ui.map.mappers
+
+import androidx.compose.runtime.Composable
+import moe.lava.banksia.core.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),
+)
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt
similarity index 62%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt
rename to ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt
index 2bc80af..aba2858 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt
@@ -1,6 +1,6 @@
-package moe.lava.banksia.ui.utils.map
+package moe.lava.banksia.ui.map.util
-import moe.lava.banksia.util.Point
+import moe.lava.banksia.core.util.Point
data class CameraPosition(
val centre: Point = Point(-37.8136, 144.9631),
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt
similarity index 50%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt
rename to ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt
index 335f668..9381262 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt
@@ -1,5 +1,5 @@
-package moe.lava.banksia.ui.utils.map
+package moe.lava.banksia.ui.map.util
-import moe.lava.banksia.util.Point
+import moe.lava.banksia.core.util.Point
data class CameraPositionBounds(val northeast: Point, val southwest: Point)
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt
new file mode 100644
index 0000000..ac33868
--- /dev/null
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt
@@ -0,0 +1,28 @@
+package moe.lava.banksia.ui.map.util
+
+import kotlinx.serialization.Serializable
+import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.core.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()
+}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt
similarity index 58%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt
rename to ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt
index d9529e4..04b8dc6 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt
+++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt
@@ -1,6 +1,6 @@
-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
+import moe.lava.banksia.core.util.Point
data class Polyline(val points: List, val colour: Color)
diff --git a/ui/shared/build.gradle.kts b/ui/shared/build.gradle.kts
new file mode 100644
index 0000000..2a78572
--- /dev/null
+++ b/ui/shared/build.gradle.kts
@@ -0,0 +1,55 @@
+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)
+ }
+
+ androidResources {
+ enable = true
+ }
+ }
+
+ 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.core)
+ }
+ }
+}
+
+dependencies {
+ androidRuntimeClasspath(libs.compose.ui.tooling)
+}
+
+compose.resources {
+ publicResClass = true
+ packageOfResClass = "moe.lava.banksia.resources"
+ generateResClass = always
+}
diff --git a/composeApp/src/androidMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.android.kt b/ui/shared/src/androidMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.android.kt
similarity index 100%
rename from composeApp/src/androidMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.android.kt
rename to ui/shared/src/androidMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.android.kt
diff --git a/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml
new file mode 100644
index 0000000..ac49572
--- /dev/null
+++ b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml
new file mode 100644
index 0000000..322fa56
--- /dev/null
+++ b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/bus.xml b/ui/shared/src/commonMain/composeResources/drawable/bus.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/bus.xml
rename to ui/shared/src/commonMain/composeResources/drawable/bus.xml
diff --git a/composeApp/src/commonMain/composeResources/drawable/bus_background.xml b/ui/shared/src/commonMain/composeResources/drawable/bus_background.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/bus_background.xml
rename to ui/shared/src/commonMain/composeResources/drawable/bus_background.xml
diff --git a/composeApp/src/commonMain/composeResources/drawable/bus_icon.xml b/ui/shared/src/commonMain/composeResources/drawable/bus_icon.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/bus_icon.xml
rename to ui/shared/src/commonMain/composeResources/drawable/bus_icon.xml
diff --git a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/ui/shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
rename to ui/shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml
diff --git a/ui/shared/src/commonMain/composeResources/drawable/my_location_24.xml b/ui/shared/src/commonMain/composeResources/drawable/my_location_24.xml
new file mode 100644
index 0000000..e69de29
diff --git a/composeApp/src/commonMain/composeResources/drawable/train.xml b/ui/shared/src/commonMain/composeResources/drawable/train.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/train.xml
rename to ui/shared/src/commonMain/composeResources/drawable/train.xml
diff --git a/composeApp/src/commonMain/composeResources/drawable/train_background.xml b/ui/shared/src/commonMain/composeResources/drawable/train_background.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/train_background.xml
rename to ui/shared/src/commonMain/composeResources/drawable/train_background.xml
diff --git a/composeApp/src/commonMain/composeResources/drawable/train_icon.xml b/ui/shared/src/commonMain/composeResources/drawable/train_icon.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/train_icon.xml
rename to ui/shared/src/commonMain/composeResources/drawable/train_icon.xml
diff --git a/composeApp/src/commonMain/composeResources/drawable/tram.xml b/ui/shared/src/commonMain/composeResources/drawable/tram.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/tram.xml
rename to ui/shared/src/commonMain/composeResources/drawable/tram.xml
diff --git a/composeApp/src/commonMain/composeResources/drawable/tram_background.xml b/ui/shared/src/commonMain/composeResources/drawable/tram_background.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/tram_background.xml
rename to ui/shared/src/commonMain/composeResources/drawable/tram_background.xml
diff --git a/composeApp/src/commonMain/composeResources/drawable/tram_icon.xml b/ui/shared/src/commonMain/composeResources/drawable/tram_icon.xml
similarity index 100%
rename from composeApp/src/commonMain/composeResources/drawable/tram_icon.xml
rename to ui/shared/src/commonMain/composeResources/drawable/tram_icon.xml
diff --git a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt
new file mode 100644
index 0000000..90914ae
--- /dev/null
+++ b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt
@@ -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.core.model.RouteType
+import moe.lava.banksia.core.model.RouteType.MetroBus
+import moe.lava.banksia.core.model.RouteType.MetroTrain
+import moe.lava.banksia.core.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)
+ }
+}
+
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt
similarity index 50%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt
rename to ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt
index c06fd1e..805f572 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt
+++ b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt
@@ -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.core.model.RouteType
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)
- }
-}
-
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt
similarity index 100%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt
rename to ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt
diff --git a/composeApp/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt b/ui/shared/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt
similarity index 100%
rename from composeApp/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt
rename to ui/shared/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
similarity index 81%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
rename to ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
index 3e41bbb..f74dc1a 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
@@ -3,8 +3,7 @@ package moe.lava.banksia.ui
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
-import moe.lava.banksia.client.di.ClientModule
-import moe.lava.banksia.di.CommonModules
+import moe.lava.banksia.ui.di.AppModule
import moe.lava.banksia.ui.screens.map.MapScreen
import org.koin.compose.KoinMultiplatformApplication
import org.koin.core.annotation.KoinExperimentalAPI
@@ -14,7 +13,7 @@ import org.koin.dsl.koinConfiguration
@Composable
fun App() {
KoinMultiplatformApplication(config = koinConfiguration {
- modules(CommonModules, ClientModule)
+ modules(AppModule)
}) {
MapScreen()
}
diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt
new file mode 100644
index 0000000..cff36fb
--- /dev/null
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt
@@ -0,0 +1,13 @@
+package moe.lava.banksia.ui.di
+
+import moe.lava.banksia.core.data.dataDiModule
+import moe.lava.banksia.ui.screens.map.MapScreenViewModel
+import org.koin.core.module.dsl.viewModelOf
+import org.koin.dsl.module
+
+val AppModule = module {
+ includes(dataDiModule)
+
+ // ViewModel
+ viewModelOf(::MapScreenViewModel)
+}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/AppBottomSheet.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/AppBottomSheet.kt
similarity index 100%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/AppBottomSheet.kt
rename to ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/AppBottomSheet.kt
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/Searcher.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/Searcher.kt
similarity index 100%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/Searcher.kt
rename to ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/Searcher.kt
diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt
new file mode 100644
index 0000000..9fb37d7
--- /dev/null
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt
@@ -0,0 +1,100 @@
+package moe.lava.banksia.ui.layout.info
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.windowInsetsBottomHeight
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.LoadingIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+import kotlin.time.Duration.Companion.milliseconds
+
+sealed class InfoPanelEvent
+
+sealed class InfoPanelState {
+ abstract val loading: Boolean
+
+ data object None : InfoPanelState() {
+ override val loading = false
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun InfoPanel(
+ modifier: Modifier = Modifier,
+ state: InfoPanelState,
+ onEvent: (InfoPanelEvent) -> Unit,
+ onPeekHeightChange: (Dp) -> Unit,
+) {
+ if (state is InfoPanelState.None)
+ return
+
+ val localDensity = LocalDensity.current
+ var delayedLoad by remember { mutableStateOf(false) }
+
+ LaunchedEffect(state.loading) {
+ if (state.loading) {
+ delay(200.milliseconds)
+ delayedLoad = true
+ } else {
+ delayedLoad = false
+ }
+ }
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp)
+ .heightIn(min = 350.dp)
+ .onSizeChanged {
+// onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
+ onPeekHeightChange(350.dp)
+ }
+ ) {
+ Box {
+ when (state) {
+ is RouteInfoPanelState -> RouteInfoPanel(state, onEvent)
+ is StopInfoPanelState -> StopInfoPanel(state, onEvent)
+ is TripInfoPanelState -> TripInfoPanel(state, onEvent)
+ is InfoPanelState.None -> throw UnsupportedOperationException()
+ }
+
+ this@Column.AnimatedVisibility(
+ modifier = Modifier.align(Alignment.TopEnd),
+ visible = delayedLoad,
+ label = "sheet-loading",
+ enter = fadeIn() + scaleIn(),
+ exit = fadeOut() + scaleOut(),
+ ) {
+ LoadingIndicator(
+ modifier = Modifier.size(48.dp)
+ )
+ }
+ }
+ Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent))
+ }
+}
diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt
new file mode 100644
index 0000000..a1a97d3
--- /dev/null
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt
@@ -0,0 +1,40 @@
+package moe.lava.banksia.ui.layout.info
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.ui.components.RouteIcon
+
+sealed class RouteInfoPanelEvent : InfoPanelEvent()
+
+data class RouteInfoPanelState(
+ val name: String,
+ val type: RouteType,
+) : InfoPanelState() {
+ override val loading = false
+}
+
+@Composable
+internal fun RouteInfoPanel(
+ state: RouteInfoPanelState,
+ onEvent: (RouteInfoPanelEvent) -> Unit,
+) {
+ Column(Modifier.fillMaxWidth()) {
+ Row {
+ RouteIcon(routeType = state.type)
+ Text(
+ state.name,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.SemiBold,
+ textAlign = TextAlign.Start
+ )
+ }
+ }
+}
diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt
new file mode 100644
index 0000000..369721c
--- /dev/null
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt
@@ -0,0 +1,358 @@
+package moe.lava.banksia.ui.layout.info
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SegmentedListItem
+import androidx.compose.material3.ShapeDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import moe.lava.banksia.resources.Res
+import moe.lava.banksia.resources.arrow_drop_down
+import moe.lava.banksia.resources.arrow_drop_up
+import moe.lava.banksia.ui.extensions.BUS_ORANGE
+import moe.lava.banksia.ui.extensions.TRAIN_BLUE
+import moe.lava.banksia.ui.platform.BanksiaTheme
+import org.jetbrains.compose.resources.painterResource
+import kotlin.time.Clock
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Instant
+
+sealed class StopInfoPanelEvent : InfoPanelEvent() {
+ data object ToggleGrouping : StopInfoPanelEvent()
+}
+
+data class StopInfoPanelState(
+ val id: String,
+ val name: String,
+ val subname: String? = null,
+ val departures: List? = null,
+) : InfoPanelState() {
+ override val loading: Boolean
+ get() = departures.isNullOrEmpty()
+
+ data class DeparturePlatforms(
+ val platform: String,
+ val departures: List,
+ )
+
+ data class DepartureInfo(
+ val routeName: String,
+ val routeColour: Color?,
+ val headsign: String,
+ val description: String?,
+ val time: Instant,
+ )
+}
+
+@Composable
+private fun listColors() = ListItemDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ selectedContainerColor = MaterialTheme.colorScheme.primary,
+ selectedContentColor = MaterialTheme.colorScheme.onPrimary,
+)
+
+@Composable
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+private fun MonoPlatform(
+ state: StopInfoPanelState.DeparturePlatforms
+) {
+ val departures = state.departures
+ val lazyState = LazyListState(firstVisibleItemIndex =
+ departures.indexOfFirst {
+ it.time > Clock.System.now()
+ }.coerceAtLeast(0)
+ )
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
+ state = lazyState,
+ ) {
+ itemsIndexed(departures) { idx, dep ->
+ SegmentedListItem(
+ onClick = {},
+ colors = listColors(),
+ shapes = ListItemDefaults.segmentedShapes(
+ idx,
+ departures.size,
+ ),
+ supportingContent = {
+ dep.description?.let { Text(dep.description) }
+ },
+ trailingContent = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy((-4).dp)
+ ) {
+ Text(
+ text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
+ style = MaterialTheme.typography.headlineSmallEmphasized,
+ )
+ Text(
+ text = "mn",
+ style = MaterialTheme.typography.labelSmallEmphasized,
+ )
+ }
+ },
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Box(
+ Modifier
+ .clip(ShapeDefaults.ExtraSmall)
+ .background(dep.routeColour ?: MaterialTheme.colorScheme.surface)
+ .padding(vertical = 2.dp, horizontal = 4.dp)
+ ) {
+ Text(
+ text = dep.routeName,
+ style = MaterialTheme.typography.labelSmallEmphasized,
+ color = MaterialTheme.colorScheme.surface,
+ )
+ }
+ Text(
+ text = dep.headsign,
+ style = MaterialTheme.typography.labelLargeEmphasized,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+private fun ManyPlatforms(
+ state: List,
+) {
+ val expandedList = remember { mutableStateListOf(*Array(state.size) { true }) }
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ state.forEachIndexed { idx, depInfo ->
+ val (platform, departures) = depInfo
+ val expanded = expandedList[idx]
+ stickyHeader(key = "header_${depInfo.hashCode()}") {
+ val base = ListItemDefaults.segmentedShapes(0, 2)
+ val large = MaterialTheme.shapes.large
+
+ Box(
+ Modifier
+ .animateItem()
+ .background(MaterialTheme.colorScheme.surfaceContainerLow)
+ .padding(bottom = ListItemDefaults.SegmentedGap)
+ ) {
+ SegmentedListItem(
+ onClick = { expandedList[idx] = !expandedList[idx] },
+ colors = listColors(),
+ shapes = if (expanded) base else base.copy(shape = large),
+ trailingContent = {
+ Icon(
+ painterResource(if (expanded) Res.drawable.arrow_drop_up else Res.drawable.arrow_drop_down),
+ contentDescription = null,
+ modifier = Modifier
+ .background(
+ if (expanded) MaterialTheme.colorScheme.surface else Color.Transparent,
+ shape = RoundedCornerShape(100)
+ )
+ .padding(6.dp),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ },
+ ) {
+ Text(
+ text = platform,
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+ }
+
+ if (expanded) {
+ item(key = "items_${depInfo.hashCode()}") {
+ Column(
+ modifier = Modifier.animateItem(),
+ verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
+ ) {
+ departures.filter { it.time > Clock.System.now() }.take(5)
+ .forEachIndexed { idx, dep ->
+ SegmentedListItem(
+ onClick = {},
+ colors = listColors(),
+ shapes = ListItemDefaults.segmentedShapes(
+ idx + 1,
+ (departures.size + 1).coerceAtMost(6),
+ ),
+ supportingContent = {
+ dep.description?.let { Text(dep.description) }
+ },
+ trailingContent = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy((-4).dp)
+ ) {
+ Text(
+ text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
+ style = MaterialTheme.typography.headlineSmallEmphasized,
+ )
+ Text(
+ text = "mn",
+ style = MaterialTheme.typography.labelSmallEmphasized,
+ )
+ }
+ },
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Box(
+ Modifier
+ .clip(ShapeDefaults.ExtraSmall)
+ .background(
+ dep.routeColour
+ ?: MaterialTheme.colorScheme.surface
+ )
+ .padding(vertical = 2.dp, horizontal = 4.dp)
+ ) {
+ Text(
+ text = dep.routeName,
+ style = MaterialTheme.typography.labelSmallEmphasized,
+ color = MaterialTheme.colorScheme.surface,
+ )
+ }
+ Text(
+ text = dep.headsign,
+ style = MaterialTheme.typography.labelLargeEmphasized,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ item(key = "spacer_${depInfo.hashCode()}") {
+ Spacer(
+ modifier = Modifier.animateItem().height(10.dp)
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+internal fun StopInfoPanel(
+ state: StopInfoPanelState,
+ onEvent: (StopInfoPanelEvent) -> Unit,
+) {
+ val spec = fadeIn(tween(300, 300)) togetherWith fadeOut(tween(300))
+
+ AnimatedContent(
+ targetState = state,
+ contentKey = { it.id },
+ transitionSpec = { spec },
+ ) { state ->
+ Column(Modifier.fillMaxWidth().fillMaxHeight()) {
+ Row {
+ Column {
+ Text(
+ state.name,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.SemiBold,
+ textAlign = TextAlign.Start
+ )
+ state.subname?.let {
+ Text(
+ "/ $it",
+ modifier = Modifier.padding(start = 5.dp),
+ style = MaterialTheme.typography.titleSmall,
+ color = Color.Gray,
+ fontWeight = FontWeight.SemiBold,
+ textAlign = TextAlign.Start
+ )
+ }
+ }
+ IconButton(
+ onClick = { onEvent(StopInfoPanelEvent.ToggleGrouping) },
+ ) { Icon(Icons.Default.Edit, null) }
+ }
+ Spacer(Modifier.height(10.dp))
+ AnimatedContent(
+ targetState = state.departures,
+ transitionSpec = { spec },
+ ) { departures ->
+ departures?.let { departurePlatforms ->
+ if (departurePlatforms.size > 1) {
+ ManyPlatforms(departurePlatforms)
+ } else if (departurePlatforms.size == 1) {
+ MonoPlatform(departurePlatforms[0])
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+internal fun StopInfoPanelPreview() {
+ fun dateIn(dur: Duration) = (Clock.System.now() + dur)
+
+ InfoPanel(
+ modifier = Modifier.background(BanksiaTheme.colors.background),
+ state = StopInfoPanelState(
+ id = "id",
+ name = "name",
+ subname = "sub",
+ departures = listOf(
+ StopInfoPanelState.DeparturePlatforms("Platform 1", listOf(
+ StopInfoPanelState.DepartureInfo("Sunbury", Color(TRAIN_BLUE), "Sunbury", "··· Malvern -> Anzac ··· Sunbury", dateIn(2.minutes)),
+ StopInfoPanelState.DepartureInfo("Sunbury", Color(TRAIN_BLUE), "West Footscray", "Express via Metro Tunnel", dateIn(8.minutes)),
+ )),
+ StopInfoPanelState.DeparturePlatforms("Platform 2", listOf(
+ StopInfoPanelState.DepartureInfo("237", Color(BUS_ORANGE), "Westall", null, dateIn(7.minutes)),
+ StopInfoPanelState.DepartureInfo("442", Color(BUS_ORANGE), "Dandenong", null, dateIn(8.minutes)),
+ )),
+ ),
+ ),
+ onEvent = {},
+ onPeekHeightChange = {},
+ )
+}
diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt
new file mode 100644
index 0000000..29bdd37
--- /dev/null
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt
@@ -0,0 +1,41 @@
+package moe.lava.banksia.ui.layout.info
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.ui.components.RouteIcon
+
+sealed class TripInfoPanelEvent : InfoPanelEvent()
+
+data class TripInfoPanelState(
+ val direction: String,
+ val type: RouteType,
+ val routeName: String? = null,
+) : InfoPanelState() {
+ override val loading = routeName == null
+}
+
+@Composable
+internal fun TripInfoPanel(
+ state: TripInfoPanelState,
+ onEvent: (TripInfoPanelEvent) -> Unit,
+) {
+ Column(Modifier.fillMaxWidth()) {
+ Row {
+ RouteIcon(routeType = state.type)
+ Text(
+ "${state.direction} via ${state.routeName ?: "..."}",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.SemiBold,
+ textAlign = TextAlign.Start
+ )
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
similarity index 84%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
rename to ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
index 15388be..1303bf5 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
@@ -35,17 +35,16 @@ import kotlinx.coroutines.launch
import moe.lava.banksia.resources.Res
import moe.lava.banksia.resources.my_location_24
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.layout.info.InfoPanel
+import moe.lava.banksia.ui.layout.info.InfoPanelState
+import moe.lava.banksia.ui.map.Maps
+import moe.lava.banksia.ui.map.rememberMapsPositionState
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(
@@ -66,6 +65,13 @@ fun MapScreen(
val sheetState = SheetStateWrapper.create()
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
+ val mapsPositionState = rememberMapsPositionState()
+ scope.launch {
+ viewModel.cameraChangeEmitter.collect {
+ mapsPositionState.update(it.value)
+ }
+ }
+
LaunchedEffect(infoState) {
if (infoState !is InfoPanelState.None) {
sheetState.peek()
@@ -78,14 +84,21 @@ 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,
+ positionState = mapsPositionState,
+// 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,
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
similarity index 66%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
rename to ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
index 99ac1fa..de06381 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
@@ -13,48 +13,57 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import moe.lava.banksia.client.repository.RouteRepository
-import moe.lava.banksia.client.repository.StopRepository
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toInstant
+import moe.lava.banksia.core.data.dto.ExtendedStopTime
+import moe.lava.banksia.core.data.repositories.RouteRepository
+import moe.lava.banksia.core.data.repositories.StopRepository
+import moe.lava.banksia.core.data.repositories.StopTimeRepository
+import moe.lava.banksia.core.model.Route
+import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.core.util.BoxedValue
+import moe.lava.banksia.core.util.BoxedValue.Companion.box
+import moe.lava.banksia.core.util.LoopFlow.Companion.waitUntilSubscribed
+import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.core.util.log
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.state.InfoPanelState
+import moe.lava.banksia.ui.extensions.getUIProperties
+import moe.lava.banksia.ui.layout.info.InfoPanelEvent
+import moe.lava.banksia.ui.layout.info.InfoPanelState
+import moe.lava.banksia.ui.layout.info.RouteInfoPanelState
+import moe.lava.banksia.ui.layout.info.StopInfoPanelEvent
+import moe.lava.banksia.ui.layout.info.StopInfoPanelState
+import moe.lava.banksia.ui.layout.info.TripInfoPanelState
+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.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?) : MapScreenEvent()
+ data class SelectStop(val id: String?) : MapScreenEvent()
data class SearchUpdate(val text: String) : MapScreenEvent()
}
-data class InternalState(
+private data class InternalState(
val route: String? = null,
- val stop: Pair? = null,
+ val stop: String? = null,
val run: String? = null,
+
+ val lastStopDepartures: List? = null,
+ val stopsGrouped: Boolean = true,
)
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) {
@@ -64,6 +73,10 @@ class MapScreenViewModel(
viewModelScope.launch { switchRoute(value.route) }
if (value.stop != last.stop)
viewModelScope.launch { switchStop(value.stop) }
+ if (value.lastStopDepartures != last.lastStopDepartures)
+ viewModelScope.launch { buildDepartures() }
+ if (value.stopsGrouped != last.stopsGrouped)
+ viewModelScope.launch { buildDepartures() }
if (value.run != last.run)
switchRun(value.run)
}
@@ -92,12 +105,20 @@ 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)
}
}
}
+ fun handleEvent(event: InfoPanelEvent) {
+ viewModelScope.launch {
+ when (event) {
+ StopInfoPanelEvent.ToggleGrouping -> state = state.copy(stopsGrouped = !state.stopsGrouped)
+ }
+ }
+ }
+
fun bindTracker(locationTracker: LocationTracker) {
locationTrackerJob = locationTracker.getLocationsFlow()
.onEach { lastKnownLocation = Point(it.latitude, it.longitude) }
@@ -105,11 +126,6 @@ class MapScreenViewModel(
}
fun centreCameraToLocation() {
- viewModelScope.launch {
- log("msvm", "getting..")
- val routes = routeRepository.getAll()
- log("msvm", routes.joinToString("\n"))
- }
lastKnownLocation?.let { location ->
viewModelScope.launch {
log("bvm", "emitting $location")
@@ -159,9 +175,9 @@ class MapScreenViewModel(
}
val route = routeRepository.get(routeId)
-// val gtfsRoute = ptvService.route(routeId)
+ ?: return
iInfoState.update {
- InfoPanelState.Route(
+ RouteInfoPanelState(
name = route.name,
type = route.type,
)
@@ -186,7 +202,7 @@ class MapScreenViewModel(
.onEach { run ->
if (routeName == null) {
iInfoState.update {
- InfoPanelState.Run(
+ TripInfoPanelState(
direction = run.destinationName,
type = RouteType.MetroTrain, // XXX HACK TODO FIXME
)
@@ -195,7 +211,7 @@ class MapScreenViewModel(
}
iInfoState.update {
- InfoPanelState.Run(
+ TripInfoPanelState(
direction = run.destinationName,
type = RouteType.MetroTrain, // FIXME HACK XXX TODO
routeName = routeName,
@@ -206,65 +222,84 @@ class MapScreenViewModel(
}
// [TODO]: Cleanup
- private suspend fun switchStop(pair: Pair?) {
- if (pair == null) {
+ private suspend fun switchStop(id: String?) {
+ if (id == null) {
iInfoState.update { InfoPanelState.None }
+ state = state.copy(lastStopDepartures = null)
return
}
- val (type, id) = pair
val stop = stopRepository.get(id)
-// val stop = ptvService.stop(routeType, stopId)
val split = stop.name.split("/")
val name = split[0]
val subname = split.getOrNull(1)
iInfoState.update {
- InfoPanelState.Stop(
+ StopInfoPanelState(
id = stop.id,
name = name,
subname = subname,
)
}
- val res = ptvService.departures(type, stop.id)
- // Map<
- // Pair,
- // Pair>
- // >
- val timetable = HashMap, Pair>>()
- 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
+ stopTimeRepository.getForStop(id)
+ .onEach { departures ->
+ state = state.copy(
+ lastStopDepartures = departures
+ )
+ }
+ .launchIn(viewModelScope)
+ }
- 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")
+ private fun friendlyPlatform(platform: String) =
+ platform.takeUnless { it.firstOrNull()?.isDigit() == true }
+ ?: "Platform $platform"
+ private fun buildDepartures() {
+ val rawDepartures = state.lastStopDepartures ?: return
+ val departures = if (state.stopsGrouped) {
+ rawDepartures
+ .groupBy { it.stopPlatformCode }
+ .mapKeys { (platform) -> platform?.let { friendlyPlatform(it) } }
+ .entries
+ .sortedBy { (platform) -> platform }
+ .map { (platform, deps) ->
+ StopInfoPanelState.DeparturePlatforms(
+ platform = platform ?: "",
+ departures = deps.map {
+ StopInfoPanelState.DepartureInfo(
+ routeName = it.routeNumber ?: it.routeName,
+ routeColour = it.routeType.getUIProperties().colour,
+ headsign = it.headsign ?: it.routeName,
+ description = null,
+ time = it.time.departure.toInstant(TimeZone.currentSystemDefault()),
+ )
+ }
+ )
+ }
+ } else if (rawDepartures.isEmpty()) {
+ listOf()
+ } else {
+ listOf(StopInfoPanelState.DeparturePlatforms(platform = "", departures = rawDepartures.map { dep ->
+ StopInfoPanelState.DepartureInfo(
+ routeName = dep.routeNumber ?: dep.routeName,
+ routeColour = dep.routeType.getUIProperties().colour,
+ headsign = dep.headsign ?: dep.routeName,
+ description = dep.stopPlatformCode?.let { friendlyPlatform(it) },
+ time = dep.time.departure.toInstant(TimeZone.currentSystemDefault()),
+ )
+ }))
}
- 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(" | "))
- }
- iInfoState.update {
- if (it !is InfoPanelState.Stop)
- it
- else
- it.copy(departures = departures)
+
+ departures.let { departures ->
+ iInfoState.update {
+ if (it !is StopInfoPanelState)
+ it
+ else
+ it.copy(departures = departures)
+ }
}
}
- 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 +329,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 +352,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,
)
}
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
similarity index 69%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
rename to ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
index ff71bf4..82ba204 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
@@ -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 = listOf(),
diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
similarity index 86%
rename from composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
rename to ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
index 05429cb..9f60514 100644
--- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
+++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
@@ -1,6 +1,6 @@
package moe.lava.banksia.ui.state
-import moe.lava.banksia.model.RouteType
+import moe.lava.banksia.core.model.RouteType
data class SearchState(
val entries: List = listOf(),
diff --git a/composeApp/src/iosMain/kotlin/moe/lava/banksia/ui/MainViewController.kt b/ui/src/iosMain/kotlin/moe/lava/banksia/ui/MainViewController.kt
similarity index 100%
rename from composeApp/src/iosMain/kotlin/moe/lava/banksia/ui/MainViewController.kt
rename to ui/src/iosMain/kotlin/moe/lava/banksia/ui/MainViewController.kt