diff --git a/.gitignore b/.gitignore
index 975a370..408e3b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,5 @@ captures
**/xcshareddata/WorkspaceSettings.xcsettings
secrets.properties
-/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt
-/data/
-/data
+shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt
+data/
diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
deleted file mode 100644
index b8b100b..0000000
--- a/androidApp/build.gradle.kts
+++ /dev/null
@@ -1,57 +0,0 @@
-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/build.gradle.kts b/build.gradle.kts
index 9434477..53d3bbb 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,12 +2,11 @@ plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
- alias(libs.plugins.androidMultiplatformLibrary) apply false
+ alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
- alias(libs.plugins.sqldelight) apply false
alias(libs.plugins.wire) apply false
}
diff --git a/ui/build.gradle.kts b/composeApp/build.gradle.kts
similarity index 55%
rename from ui/build.gradle.kts
rename to composeApp/build.gradle.kts
index b599bc6..68df64c 100644
--- a/ui/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -1,31 +1,27 @@
+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.androidMultiplatformLibrary)
+ alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.secretsGradle)
+ alias(libs.plugins.spm)
}
kotlin {
- android {
- namespace = "moe.lava.banksia.ui"
- compileSdk = libs.versions.android.compileSdk.get().toInt()
-
+ androidTarget {
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
-
- androidResources {
- enable = true
- }
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
- freeCompilerArgs.add("-Xexplicit-backing-fields")
}
listOf(
@@ -33,17 +29,26 @@ 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)
@@ -67,21 +72,61 @@ kotlin {
implementation(libs.maplibre.compose)
implementation(libs.moko.geo)
implementation(libs.moko.geo.compose)
+ implementation(projects.shared)
implementation(libs.ui.backhandler)
-
- implementation(projects.core)
- implementation(projects.core.data)
- implementation(projects.core.stoptime)
- implementation(projects.ui.maps)
- implementation(projects.ui.shared)
}
}
}
+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
+ }
+}
+
dependencies {
- androidRuntimeClasspath(libs.compose.ui.tooling)
+ debugImplementation(compose.uiTooling)
}
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/androidApp/src/main/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
similarity index 91%
rename from androidApp/src/main/AndroidManifest.xml
rename to composeApp/src/androidMain/AndroidManifest.xml
index 16435e6..928349e 100644
--- a/androidApp/src/main/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -13,6 +13,9 @@
android:enableOnBackInvokedCallback="true"
android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
+
+
+
+
+
diff --git a/ui/shared/src/commonMain/composeResources/drawable/train.xml b/composeApp/src/commonMain/composeResources/drawable/train.xml
similarity index 100%
rename from ui/shared/src/commonMain/composeResources/drawable/train.xml
rename to composeApp/src/commonMain/composeResources/drawable/train.xml
diff --git a/ui/shared/src/commonMain/composeResources/drawable/train_background.xml b/composeApp/src/commonMain/composeResources/drawable/train_background.xml
similarity index 100%
rename from ui/shared/src/commonMain/composeResources/drawable/train_background.xml
rename to composeApp/src/commonMain/composeResources/drawable/train_background.xml
diff --git a/ui/shared/src/commonMain/composeResources/drawable/train_icon.xml b/composeApp/src/commonMain/composeResources/drawable/train_icon.xml
similarity index 100%
rename from ui/shared/src/commonMain/composeResources/drawable/train_icon.xml
rename to composeApp/src/commonMain/composeResources/drawable/train_icon.xml
diff --git a/ui/shared/src/commonMain/composeResources/drawable/tram.xml b/composeApp/src/commonMain/composeResources/drawable/tram.xml
similarity index 100%
rename from ui/shared/src/commonMain/composeResources/drawable/tram.xml
rename to composeApp/src/commonMain/composeResources/drawable/tram.xml
diff --git a/ui/shared/src/commonMain/composeResources/drawable/tram_background.xml b/composeApp/src/commonMain/composeResources/drawable/tram_background.xml
similarity index 100%
rename from ui/shared/src/commonMain/composeResources/drawable/tram_background.xml
rename to composeApp/src/commonMain/composeResources/drawable/tram_background.xml
diff --git a/ui/shared/src/commonMain/composeResources/drawable/tram_icon.xml b/composeApp/src/commonMain/composeResources/drawable/tram_icon.xml
similarity index 100%
rename from ui/shared/src/commonMain/composeResources/drawable/tram_icon.xml
rename to composeApp/src/commonMain/composeResources/drawable/tram_icon.xml
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
new file mode 100644
index 0000000..bfbb204
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/local/RouteLocalDataSource.kt
@@ -0,0 +1,11 @@
+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
new file mode 100644
index 0000000..1b418a0
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/local/StopLocalDataSource.kt
@@ -0,0 +1,12 @@
+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
new file mode 100644
index 0000000..861a3d8
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/RouteRemoteDataSource.kt
@@ -0,0 +1,11 @@
+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/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/StopRemoteDataSource.kt
similarity index 64%
rename from core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/StopRemoteDataSource.kt
index f39afd3..f708cec 100644
--- a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/datasource/remote/StopRemoteDataSource.kt
@@ -1,11 +1,11 @@
-package moe.lava.banksia.core.data.sources.stop
+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.core.model.Stop
+import moe.lava.banksia.model.Stop
-internal class StopRemoteDataSource(val client: HttpClient) {
+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/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt
similarity index 59%
rename from core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt
index 104c6bc..2002745 100644
--- a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.data
+package moe.lava.banksia.client.di
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpSend
@@ -7,22 +7,21 @@ 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.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.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.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.dsl.bind
+import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
-actual val platformModule = module {
+val ClientModule = module {
// HTTP Clients
singleOf(::PtvService)
single {
@@ -51,6 +50,9 @@ actual val platformModule = module {
singleOf(::StopRemoteDataSource)
// Repositories
- singleOf(::ClientRouteRepository) bind RouteRepository::class
- singleOf(::ClientStopRepository) bind StopRepository::class
+ singleOf(::RouteRepository)
+ singleOf(::StopRepository)
+
+ // ViewModel
+ viewModelOf(::MapScreenViewModel)
}
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
new file mode 100644
index 0000000..49e397d
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/repository/RouteRepository.kt
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000..c9eedce
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopRepository.kt
@@ -0,0 +1,22 @@
+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/ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
similarity index 81%
rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
index f74dc1a..3e41bbb 100644
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt
@@ -3,7 +3,8 @@ package moe.lava.banksia.ui
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
-import moe.lava.banksia.ui.di.AppModule
+import moe.lava.banksia.client.di.ClientModule
+import moe.lava.banksia.di.CommonModules
import moe.lava.banksia.ui.screens.map.MapScreen
import org.koin.compose.KoinMultiplatformApplication
import org.koin.core.annotation.KoinExperimentalAPI
@@ -13,7 +14,7 @@ import org.koin.dsl.koinConfiguration
@Composable
fun App() {
KoinMultiplatformApplication(config = koinConfiguration {
- modules(AppModule)
+ modules(CommonModules, ClientModule)
}) {
MapScreen()
}
diff --git a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt
similarity index 50%
rename from ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt
index 805f572..c06fd1e 100644
--- a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt
@@ -1,8 +1,27 @@
-package moe.lava.banksia.ui.extensions
+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.graphics.Color
-import moe.lava.banksia.core.model.RouteType
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import moe.lava.banksia.data.ptv.structures.PtvRouteType
+import moe.lava.banksia.model.RouteType
+import moe.lava.banksia.model.RouteType.Interstate
+import moe.lava.banksia.model.RouteType.MetroBus
+import moe.lava.banksia.model.RouteType.MetroTrain
+import moe.lava.banksia.model.RouteType.MetroTram
+import moe.lava.banksia.model.RouteType.RegionalBus
+import moe.lava.banksia.model.RouteType.RegionalCoach
+import moe.lava.banksia.model.RouteType.RegionalTrain
+import moe.lava.banksia.model.RouteType.SkyBus
import moe.lava.banksia.resources.Res
import moe.lava.banksia.resources.bus
import moe.lava.banksia.resources.bus_background
@@ -14,6 +33,7 @@ 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,
@@ -29,31 +49,31 @@ const val VLINE_PURPLE = 0xFF8F1A95
fun RouteType.getUIProperties(): RouteTypeProperties {
val colour = when (this) {
- 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
+ MetroTrain -> TRAIN_BLUE
+ MetroTram -> TRAM_GREEN
+ MetroBus -> BUS_ORANGE
+ RegionalTrain -> VLINE_PURPLE
+ RegionalCoach -> VLINE_PURPLE
+ RegionalBus -> VLINE_PURPLE
+ SkyBus -> BUS_ORANGE
+ Interstate -> BUS_ORANGE
}
val (drawable, background, icon) = when (this) {
- RouteType.MetroTrain,
- RouteType.RegionalTrain,
- RouteType.Interstate -> Triple(
+ MetroTrain,
+ RegionalTrain,
+ Interstate -> Triple(
Res.drawable.train, Res.drawable.train_background, Res.drawable.train_icon
)
- RouteType.MetroTram -> Triple(
+ MetroTram -> Triple(
Res.drawable.tram, Res.drawable.tram_background, Res.drawable.tram_icon
)
- RouteType.MetroBus,
- RouteType.RegionalCoach,
- RouteType.RegionalBus,
- RouteType.SkyBus -> Triple(
+ MetroBus,
+ RegionalCoach,
+ RegionalBus,
+ SkyBus -> Triple(
Res.drawable.bus, Res.drawable.bus_background, Res.drawable.bus_icon
)
}
@@ -82,3 +102,35 @@ 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/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/AppBottomSheet.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/AppBottomSheet.kt
similarity index 100%
rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/AppBottomSheet.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/AppBottomSheet.kt
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
new file mode 100644
index 0000000..8d525f3
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt
@@ -0,0 +1,177 @@
+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/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/Searcher.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/Searcher.kt
similarity index 100%
rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/Searcher.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/layout/Searcher.kt
diff --git a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt
similarity index 100%
rename from ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt
diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
similarity index 84%
rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
index 1303bf5..15388be 100644
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt
@@ -35,16 +35,17 @@ 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(
@@ -65,13 +66,6 @@ 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()
@@ -84,21 +78,14 @@ fun MapScreen(
Scaffold {
Maps(
modifier = Modifier.fillMaxSize(),
- insets = WindowInsets(top = with(LocalDensity.current) {
+ state = mapState,
+ onEvent = viewModel::handleEvent,
+ cameraPositionFlow = viewModel.cameraChangeEmitter,
+ extInsets = WindowInsets(top = with(LocalDensity.current) {
SearchBarDefaults.InputFieldHeight.roundToPx()
}, bottom = sheetState.bottomInset),
- stops = mapState.stops,
- positionState = mapsPositionState,
-// vehicles = mapState.vehicles,
- onStopClicked = { stop ->
- viewModel.handleEvent(MapScreenEvent.SelectStop(stop))
- },
-// onEvent = viewModel::handleEvent,
-// cameraPositionFlow = viewModel.cameraChangeEmitter,
-// setLastKnownLocation = viewModel::setLastKnownLocation,
+ setLastKnownLocation = viewModel::setLastKnownLocation,
)
-
-// onEvent()
Searcher(
state = searchState,
onEvent = viewModel::handleEvent,
diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
similarity index 66%
rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
index de06381..99ac1fa 100644
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt
@@ -13,57 +13,48 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-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.client.repository.RouteRepository
+import moe.lava.banksia.client.repository.StopRepository
import moe.lava.banksia.data.ptv.PtvService
-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.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.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 id: String?) : MapScreenEvent()
+ data class SelectStop(val typeIdPair: Pair?) : MapScreenEvent()
data class SearchUpdate(val text: String) : MapScreenEvent()
}
-private data class InternalState(
+data class InternalState(
val route: String? = null,
- val stop: String? = null,
+ val stop: Pair? = 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) {
@@ -73,10 +64,6 @@ 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)
}
@@ -105,20 +92,12 @@ 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.id, run = null)
+ is MapScreenEvent.SelectStop -> state = state.copy(stop = event.typeIdPair, 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) }
@@ -126,6 +105,11 @@ 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")
@@ -175,9 +159,9 @@ class MapScreenViewModel(
}
val route = routeRepository.get(routeId)
- ?: return
+// val gtfsRoute = ptvService.route(routeId)
iInfoState.update {
- RouteInfoPanelState(
+ InfoPanelState.Route(
name = route.name,
type = route.type,
)
@@ -202,7 +186,7 @@ class MapScreenViewModel(
.onEach { run ->
if (routeName == null) {
iInfoState.update {
- TripInfoPanelState(
+ InfoPanelState.Run(
direction = run.destinationName,
type = RouteType.MetroTrain, // XXX HACK TODO FIXME
)
@@ -211,7 +195,7 @@ class MapScreenViewModel(
}
iInfoState.update {
- TripInfoPanelState(
+ InfoPanelState.Run(
direction = run.destinationName,
type = RouteType.MetroTrain, // FIXME HACK XXX TODO
routeName = routeName,
@@ -222,84 +206,65 @@ class MapScreenViewModel(
}
// [TODO]: Cleanup
- private suspend fun switchStop(id: String?) {
- if (id == null) {
+ private suspend fun switchStop(pair: Pair?) {
+ if (pair == 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 {
- StopInfoPanelState(
+ InfoPanelState.Stop(
id = stop.id,
name = name,
subname = subname,
)
}
- stopTimeRepository.getForStop(id)
- .onEach { departures ->
- state = state.copy(
- lastStopDepartures = departures
- )
- }
- .launchIn(viewModelScope)
- }
+ 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
- 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 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")
}
-
- departures.let { departures ->
- iInfoState.update {
- if (it !is StopInfoPanelState)
- it
- else
- it.copy(departures = departures)
- }
+ 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)
}
}
- /*private suspend fun buildPolylines(route: PtvRoute) {
+ private suspend fun buildPolylines(route: PtvRoute) {
val routeWithGeo = if (route.geopath.isEmpty())
ptvService.route(route.routeId, true)
else
@@ -329,9 +294,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)
@@ -352,16 +317,19 @@ 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/screens/map/Maps.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/Maps.kt
new file mode 100644
index 0000000..fe20f9f
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/Maps.kt
@@ -0,0 +1,210 @@
+@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
new file mode 100644
index 0000000..b0acbec
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt
@@ -0,0 +1,38 @@
+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/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
similarity index 69%
rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
index 82ba204..ff71bf4 100644
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt
+++ b/composeApp/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.map.util.Marker
-import moe.lava.banksia.ui.map.util.Polyline
+import moe.lava.banksia.ui.utils.map.Marker
+import moe.lava.banksia.ui.utils.map.Polyline
data class MapState(
val stops: List = listOf(),
diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
similarity index 86%
rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
index 9f60514..05429cb 100644
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt
@@ -1,6 +1,6 @@
package moe.lava.banksia.ui.state
-import moe.lava.banksia.core.model.RouteType
+import moe.lava.banksia.model.RouteType
data class SearchState(
val entries: List = listOf(),
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt
similarity index 62%
rename from ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt
index aba2858..2bc80af 100644
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt
@@ -1,6 +1,6 @@
-package moe.lava.banksia.ui.map.util
+package moe.lava.banksia.ui.utils.map
-import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.util.Point
data class CameraPosition(
val centre: Point = Point(-37.8136, 144.9631),
diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt
similarity index 50%
rename from ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt
index 9381262..335f668 100644
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt
@@ -1,5 +1,5 @@
-package moe.lava.banksia.ui.map.util
+package moe.lava.banksia.ui.utils.map
-import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.util.Point
data class CameraPositionBounds(val northeast: Point, val southwest: Point)
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
new file mode 100644
index 0000000..2efe33d
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Marker.kt
@@ -0,0 +1,22 @@
+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/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt
similarity index 58%
rename from ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt
rename to composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt
index 04b8dc6..d9529e4 100644
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt
+++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt
@@ -1,6 +1,6 @@
-package moe.lava.banksia.ui.map.util
+package moe.lava.banksia.ui.utils.map
import androidx.compose.ui.graphics.Color
-import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.util.Point
data class Polyline(val points: List, val colour: Color)
diff --git a/ui/src/iosMain/kotlin/moe/lava/banksia/ui/MainViewController.kt b/composeApp/src/iosMain/kotlin/moe/lava/banksia/ui/MainViewController.kt
similarity index 100%
rename from ui/src/iosMain/kotlin/moe/lava/banksia/ui/MainViewController.kt
rename to composeApp/src/iosMain/kotlin/moe/lava/banksia/ui/MainViewController.kt
diff --git a/ui/shared/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt b/composeApp/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt
similarity index 100%
rename from ui/shared/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt
rename to composeApp/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt
diff --git a/composeApp/src/swift/spmMaplibre/StartYourBridgeHere.swift b/composeApp/src/swift/spmMaplibre/StartYourBridgeHere.swift
new file mode 100644
index 0000000..d53c4d7
--- /dev/null
+++ b/composeApp/src/swift/spmMaplibre/StartYourBridgeHere.swift
@@ -0,0 +1,15 @@
+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/core/data/build.gradle.kts b/core/data/build.gradle.kts
deleted file mode 100644
index ecdba19..0000000
--- a/core/data/build.gradle.kts
+++ /dev/null
@@ -1,64 +0,0 @@
-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/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
deleted file mode 100644
index f46caac..0000000
--- a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-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
deleted file mode 100644
index 0aee84e..0000000
--- a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-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
deleted file mode 100644
index 8286b1f..0000000
--- a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-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
deleted file mode 100644
index 15088fb..0000000
--- a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-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
deleted file mode 100644
index 524d123..0000000
--- a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-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/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
deleted file mode 100644
index eea6a0e..0000000
--- a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index ef3d6f1..0000000
--- a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-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
deleted file mode 100644
index c663f89..0000000
--- a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-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
deleted file mode 100644
index 78a44d1..0000000
--- a/core/data/src/jvmMain/kotlin/moe/lava/banksia/core/data/DataDiModule.jvm.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-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
deleted file mode 100644
index 472a908..0000000
--- a/core/sqld/build.gradle.kts
+++ /dev/null
@@ -1,53 +0,0 @@
-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
deleted file mode 100644
index c47613c..0000000
--- a/core/sqld/src/androidMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.android.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-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
deleted file mode 100644
index 983eb58..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-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
deleted file mode 100644
index deee453..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/SqldDiModule.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-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
deleted file mode 100644
index f3a5521..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Route.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-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
deleted file mode 100644
index dbda5ea..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Service.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-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
deleted file mode 100644
index ef0d201..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/ServiceException.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-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
deleted file mode 100644
index 4a8d7db..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Shape.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-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
deleted file mode 100644
index 3bf6b54..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Stop.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-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
deleted file mode 100644
index 26d5390..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StopTime.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-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
deleted file mode 100644
index d1409a2..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StoppingPattern.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-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
deleted file mode 100644
index b3443fb..0000000
--- a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Trip.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-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
deleted file mode 100644
index e607975..0000000
--- a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Route.sq
+++ /dev/null
@@ -1,20 +0,0 @@
-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
deleted file mode 100644
index a1c5fad..0000000
--- a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Service.sq
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index 332f198..0000000
--- a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/ServiceException.sq
+++ /dev/null
@@ -1,9 +0,0 @@
-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
deleted file mode 100644
index 8734200..0000000
--- a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Shape.sq
+++ /dev/null
@@ -1,7 +0,0 @@
-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
deleted file mode 100644
index 4af5c50..0000000
--- a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Stop.sq
+++ /dev/null
@@ -1,54 +0,0 @@
-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
deleted file mode 100644
index 06bd76b..0000000
--- a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StopTime.sq
+++ /dev/null
@@ -1,45 +0,0 @@
-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
deleted file mode 100644
index 9a09e69..0000000
--- a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StoppingPattern.sq
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index c53b62a..0000000
--- a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Trip.sq
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index feaacb3..0000000
Binary files a/core/sqld/src/commonMain/sqldelight/schema/1.db and /dev/null 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
deleted file mode 100644
index 9ce0627..0000000
--- a/core/sqld/src/iosMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.ios.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index 61d9e95..0000000
--- a/core/sqld/src/jvmMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.jvm.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-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/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt
deleted file mode 100644
index 7e23b5d..0000000
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package moe.lava.banksia.core.endpoints
-
-object Endpoint
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
deleted file mode 100644
index ef2f918..0000000
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/ServiceException.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-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/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt
deleted file mode 100644
index edd7c51..0000000
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-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
deleted file mode 100644
index 1374cff..0000000
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StoppingPattern.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-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
deleted file mode 100644
index 752d6d2..0000000
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Trip.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-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/core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt
deleted file mode 100644
index 7feca0d..0000000
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-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/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
deleted file mode 100644
index 014c1d2..0000000
--- a/core/src/iosMain/kotlin/moe/lava/banksia/core/util/Logging.ios.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-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/core/stoptime/build.gradle.kts b/core/stoptime/build.gradle.kts
deleted file mode 100644
index 44cf072..0000000
--- a/core/stoptime/build.gradle.kts
+++ /dev/null
@@ -1,64 +0,0 @@
-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
deleted file mode 100644
index 2f83304..0000000
--- a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.client.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index ecaff8e..0000000
--- a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.client.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-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
deleted file mode 100644
index 0c38f64..0000000
--- a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-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
deleted file mode 100644
index d46affa..0000000
--- a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index 38de29d..0000000
--- a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/dto/ExtendedStopTime.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-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
deleted file mode 100644
index 6a81c09..0000000
--- a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-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
deleted file mode 100644
index f22dc09..0000000
--- a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-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
deleted file mode 100644
index f689b2d..0000000
--- a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/StopTimeEndpoints.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-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
deleted file mode 100644
index 70ef406..0000000
--- a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.jvm.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-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
deleted file mode 100644
index b4c37a6..0000000
--- a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.jvm.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index 5791855..0000000
--- a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/server/routes/StopTimeRoute.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-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 f0cf36f..80feb8f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,3 +12,5 @@ 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
deleted file mode 100644
index 9b7b12a..0000000
--- a/gradle/gradle-daemon-jvm.properties
+++ /dev/null
@@ -1,13 +0,0 @@
-#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 483c5d5..b54a4eb 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,36 +1,41 @@
[versions]
-agp = "9.1.0"
-android-compileSdk = "37"
+agp = "8.13.1"
+android-compileSdk = "36"
android-minSdk = "24"
-android-targetSdk = "37"
-androidx-activity= "1.13.0"
-androidx-lifecycle = "2.10.0"
-compose-multiplatform = "1.12.0-alpha02"
+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"
composeunstyled = "1.49.6"
coroutines = "1.10.2"
geo = "0.8.0"
-koin = "4.2.0"
-kotlin = "2.3.20"
+junit = "4.13.2"
+koin = "4.1.1"
+kotlin = "2.3.10"
kotlinxDatetime = "0.7.1"
kotlinxSerializationCsv = "0.2.18"
kotlinxSerialization = "1.10.0"
ksp = "2.3.4"
-ktor = "3.4.1"
+ktor = "3.4.0"
logback = "1.5.32"
maplibre = "0.12.1"
material = "1.7.3"
-material3 = "1.11.0-alpha07"
-okio = "3.17.0"
+material3 = "1.11.0-alpha02"
+okio = "3.16.4"
playServicesLocation = "21.3.0"
+room = "2.8.4"
secretsGradlePlugin = "2.0.1"
-sqldelight = "2.3.2"
-wire = "6.1.0"
+spm = "1.4.9"
+sqlite = "2.6.2"
+wire = "5.5.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" }
@@ -40,11 +45,18 @@ 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" }
@@ -62,19 +74,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" }
-androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
+androidLibrary = { id = "com.android.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" }
@@ -82,6 +94,7 @@ 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" }
-sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
+spm = { id = "io.github.frankois944.spmForKmp", version.ref = "spm" }
wire = { id = "com.squareup.wire", version.ref = "wire" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 37f78a6..37f853b 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-9.3.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
index 412e378..22d2bc6 100644
--- a/iosApp/iosApp/Info.plist
+++ b/iosApp/iosApp/Info.plist
@@ -46,5 +46,7 @@
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 9d2cb78..2f7d989 100644
--- a/server/build.gradle.kts
+++ b/server/build.gradle.kts
@@ -5,27 +5,15 @@ plugins {
application
}
-group = "moe.lava.banksia.server"
+group = "moe.lava.banksia"
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.core)
- implementation(projects.core.data)
- implementation(projects.core.sqld)
- implementation(projects.core.stoptime)
- implementation(projects.server.gtfs)
- implementation(projects.server.gtfsRt)
-
+ implementation(projects.shared)
implementation(libs.logback)
implementation(libs.koin.core)
implementation(libs.koin.ktor)
@@ -38,6 +26,8 @@ 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
deleted file mode 100644
index 8f6d646..0000000
--- a/server/gtfs/build.gradle.kts
+++ /dev/null
@@ -1,20 +0,0 @@
-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
deleted file mode 100644
index c844499..0000000
--- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt
+++ /dev/null
@@ -1,388 +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.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/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
deleted file mode 100644
index 1bf9573..0000000
--- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-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
deleted file mode 100644
index a31aff0..0000000
--- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsServiceException.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-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/gtfs_rt/build.gradle.kts b/server/gtfs_rt/build.gradle.kts
deleted file mode 100644
index 2887e0b..0000000
--- a/server/gtfs_rt/build.gradle.kts
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index 128f141..0000000
--- a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsRealtime.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-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
deleted file mode 100644
index aaee0a9..0000000
--- a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtArchiver.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-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
deleted file mode 100644
index 6f46ed7..0000000
--- a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtService.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-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
deleted file mode 100644
index 4466b91..0000000
--- a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/RealtimeVehiclePositions.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-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/server/src/main/kotlin/moe/lava/banksia/server/Application.kt b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt
index dedffe5..4ae3398 100644
--- a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt
+++ b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt
@@ -15,59 +15,37 @@ import io.ktor.server.routing.routing
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-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.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.server.di.ServerModules
-import moe.lava.banksia.server.gtfsrt.GtfsrtService
-import moe.lava.banksia.server.routes.stopTimeRoutes
+import moe.lava.banksia.server.gtfs.GtfsHandler
+import moe.lava.banksia.server.gtfsr.GtfsrService
import org.koin.dsl.module
-import org.koin.ktor.ext.get
+import org.koin.ktor.ext.inject
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(ServerModules)
+ modules(CommonModules, ServerModules)
}
- @Suppress("KotlinConstantConditions")
- launch { get().start(this, !Constants.devMode) }
+ val gtfsr by inject()
+ launch { gtfsr.start() }
routing {
- 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") {
+ get("/update") {
val key = call.parameters["key"]
if (key != Constants.updateKey) {
call.respond(HttpStatusCode.Forbidden)
@@ -79,14 +57,30 @@ 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) {
- get().import(datasetUrl)
- get().addParentsToStops()
+ 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("/routes") {
val routes = withContext(context = Dispatchers.IO) {
- get().getAll().executeAsList()
+ inject().value.getAll()
}
val res = routes.map { it.asModel() }
call.respond(res)
@@ -94,17 +88,16 @@ fun Application.module() {
get("/routes/{route_id}") {
val routeId = call.parameters["route_id"]!!
val route = withContext(context = Dispatchers.IO) {
- get().get(routeId).executeAsOneOrNull()
+ inject().value.get(routeId)
}
- if (route != null) {
+ if (route != null)
call.respond(route.asModel())
- } else {
+ else
call.respond(HttpStatusCode.NotFound)
- }
}
get("/stops") {
val routes = withContext(context = Dispatchers.IO) {
- get().getAll().executeAsList()
+ inject().value.getAll()
}
val res = routes.map { it.asModel() }
call.respond(res)
@@ -112,26 +105,38 @@ fun Application.module() {
get("/stops/{stop_id}") {
val stopId = call.parameters["stop_id"]!!
val stop = withContext(context = Dispatchers.IO) {
- get().get(stopId).executeAsOneOrNull()
+ inject().value.get(stopId)
}
- 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("false", "0")
+ val useParent = call.queryParameters["parent"] in listOf("true", "1")
val stops = withContext(Dispatchers.IO) {
- val queries = get()
- if (useParent) {
- queries.getParentsByRoute(routeId).executeAsList()
- } else {
- queries.getByRoute(routeId).executeAsList()
- }
+ val routeDao by inject()
+ if (useParent)
+ routeDao.stopsParent(routeId)
+ else
+ routeDao.stops(routeId)
}
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
deleted file mode 100644
index 97892e0..0000000
--- a/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-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
deleted file mode 100644
index 84fae70..0000000
--- a/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-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 b2593b3..c7b650c 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,22 +1,13 @@
package moe.lava.banksia.server.di
import io.ktor.client.HttpClient
-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 moe.lava.banksia.server.gtfs.GtfsHandler
+import moe.lava.banksia.server.gtfsr.GtfsrService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val ServerModules = module {
- includes(dataDiModule)
-
single { HttpClient() }
- singleOf(::GtfsParser)
- singleOf(::GtfsrtService)
-
- factoryOf(::GtfsDataFixer)
- factoryOf(::GtfsImporter)
+ singleOf(::GtfsHandler)
+ singleOf(::GtfsrService)
}
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
new file mode 100644
index 0000000..d85d5df
--- /dev/null
+++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt
@@ -0,0 +1,294 @@
+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/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
similarity index 91%
rename from server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
rename to server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
index 4b1bad9..c4eabeb 100644
--- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
+++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
-internal data class GtfsRoute(
+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/GtfsShape.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
similarity index 90%
rename from server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
rename to server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
index 32231ab..19cdfb5 100644
--- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
+++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
-internal data class GtfsShape(
+data class GtfsShape(
val shape_id: String,
val shape_pt_lat: Double,
val shape_pt_lon: Double,
diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
similarity index 92%
rename from server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
rename to server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
index cb1a018..023a3e1 100644
--- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
+++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
-internal data class GtfsStop(
+data class GtfsStop(
val stop_id: String,
val stop_name: String,
val stop_lat: Double,
diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
similarity index 84%
rename from server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
rename to server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
index c0bbaf2..61e8a1c 100644
--- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt
+++ b/server/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.core.model.FutureTime
+import moe.lava.banksia.model.FutureTime
@Suppress("PropertyName")
@Serializable
-internal data class GtfsStopTime(
+data class GtfsStopTime(
val trip_id: String,
val arrival_time: String,
val departure_time: String,
val stop_id: String,
- val stop_sequence: Long,
+ val stop_sequence: Int,
val stop_headsign: String,
val pickup_type: Int,
val drop_off_type: Int,
diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
similarity index 92%
rename from server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
rename to server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
index 0b0d865..fcfc864 100644
--- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
+++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
-internal data class GtfsTrip(
+data class GtfsTrip(
val route_id: String,
val service_id: String,
val trip_id: String,
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
new file mode 100644
index 0000000..5a0b1dc
--- /dev/null
+++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfsr/GtfsrService.kt
@@ -0,0 +1,164 @@
+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 6519371..de5d8bf 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 bdb499e..3649a7a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -14,9 +14,6 @@ pluginManagement {
gradlePluginPortal()
}
}
-plugins {
- id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
-}
dependencyResolutionManagement {
repositories {
@@ -31,14 +28,6 @@ dependencyResolutionManagement {
}
}
-include(":androidApp")
+include(":composeApp")
include(":server")
-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")
+include(":shared")
diff --git a/core/build.gradle.kts b/shared/build.gradle.kts
similarity index 52%
rename from core/build.gradle.kts
rename to shared/build.gradle.kts
index 3dd2ee6..1f26a53 100644
--- a/core/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -1,16 +1,22 @@
+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.androidMultiplatformLibrary)
+ alias(libs.plugins.androidLibrary)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.room)
+ alias(libs.plugins.wire)
+}
+
+room {
+ schemaDirectory("$projectDir/schemas")
}
kotlin {
- android {
- namespace = "moe.lava.banksia.core"
- compileSdk = libs.versions.android.compileSdk.get().toInt()
-
+ androidTarget {
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
@@ -20,6 +26,7 @@ kotlin {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
+ iosX64()
iosArm64()
iosSimulatorArm64()
@@ -40,9 +47,38 @@ 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/shared/schemas/moe.lava.banksia.room.Database/1.json b/shared/schemas/moe.lava.banksia.room.Database/1.json
new file mode 100644
index 0000000..037062e
--- /dev/null
+++ b/shared/schemas/moe.lava.banksia.room.Database/1.json
@@ -0,0 +1,72 @@
+{
+ "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
new file mode 100644
index 0000000..04a14e3
--- /dev/null
+++ b/shared/schemas/moe.lava.banksia.room.Database/2.json
@@ -0,0 +1,315 @@
+{
+ "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
new file mode 100644
index 0000000..e769926
--- /dev/null
+++ b/shared/schemas/moe.lava.banksia.room.Database/3.json
@@ -0,0 +1,339 @@
+{
+ "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
new file mode 100644
index 0000000..0447f4b
--- /dev/null
+++ b/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt
@@ -0,0 +1,25 @@
+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/core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt b/shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt
similarity index 87%
rename from core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt
rename to shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt
index e0b792e..31c3072 100644
--- a/core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt
+++ b/shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.util
+package moe.lava.banksia.util
import android.util.Log
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton b/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton
similarity index 78%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton
rename to shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton
index 909f642..7329ae3 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton
@@ -6,7 +6,6 @@ object Constants {
const val opendataKey: String = ""
const val serverUrl: String = "https://banksia.lava.moe/api/"
// TODO
- var devMode: Boolean = false
+ const val devMode: Boolean = false
const val updateKey: String = ""
- const val protomapsKey: String = ""
}
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
similarity index 97%
rename from core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
index 54717a2..77ab12d 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt
@@ -16,12 +16,7 @@ import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
-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.Constants
import moe.lava.banksia.data.ptv.structures.PtvDeparture
import moe.lava.banksia.data.ptv.structures.PtvDirection
import moe.lava.banksia.data.ptv.structures.PtvRoute
@@ -29,6 +24,11 @@ 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/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt
similarity index 100%
rename from core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt
similarity index 100%
rename from core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt
similarity index 100%
rename from core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
similarity index 94%
rename from core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
index 4aae762..3178328 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt
+++ b/shared/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.core.model.RouteType
+import moe.lava.banksia.model.RouteType
@Serializable
data class PtvRoute(
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
similarity index 93%
rename from core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
index d8808f1..0726665 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt
+++ b/shared/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.core.model.RouteType
+import moe.lava.banksia.model.RouteType
-object PtvRouteTypeSerialiser : KSerializer {
+private object PtvRouteTypeSerialiser : KSerializer {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
PtvRouteType::class.qualifiedName!!,
PrimitiveKind.INT)
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt
similarity index 100%
rename from core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt
similarity index 100%
rename from core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt
new file mode 100644
index 0000000..823174b
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt
@@ -0,0 +1,16 @@
+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
new file mode 100644
index 0000000..6f29f14
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt
@@ -0,0 +1,21 @@
+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/core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt
similarity index 81%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt
index 7c77309..c1853a9 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt
@@ -1,10 +1,6 @@
-package moe.lava.banksia.core.model
+package moe.lava.banksia.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
@@ -12,7 +8,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.core.model.FutureTime.Companion.asInt
+import moe.lava.banksia.model.FutureTime.Companion.asInt
@Serializable(FutureTimeSerialiser::class)
data class FutureTime(
@@ -43,10 +39,6 @@ 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/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt
similarity index 82%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt
index b2741f4..9cfff0f 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.model
+package moe.lava.banksia.model
import kotlinx.serialization.Serializable
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt
similarity index 66%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt
index 86555a6..08a9c53 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.model
+package moe.lava.banksia.model
import kotlinx.serialization.Serializable
@@ -13,8 +13,4 @@ enum class RouteType(val value: Int) {
SkyBus(11),
Interstate(10),
;
-
- companion object {
- fun from(value: Int) = entries.first { it.value == value }
- }
}
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt
similarity index 52%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt
index 69799bf..328a4b0 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.model
+package moe.lava.banksia.model
data class Run(
val ref: String,
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt
similarity index 87%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt
index 8568397..a57fb82 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.model
+package moe.lava.banksia.model
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt
similarity index 67%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt
index 7b71427..6299ca0 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt
@@ -1,7 +1,7 @@
-package moe.lava.banksia.core.model
+package moe.lava.banksia.model
import kotlinx.serialization.Serializable
-import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.util.Point
typealias ShapePath = List
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt
similarity index 53%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt
index bbe6fbf..df10a58 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt
@@ -1,15 +1,15 @@
-package moe.lava.banksia.core.model
+package moe.lava.banksia.model
import kotlinx.serialization.Serializable
-import moe.lava.banksia.core.util.Point
+import moe.lava.banksia.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/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt
new file mode 100644
index 0000000..682839d
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 0000000..ef95eea
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt
@@ -0,0 +1,15 @@
+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/core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt
similarity index 79%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt
index 2ee4f28..1770b23 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.model
+package moe.lava.banksia.model
import kotlinx.serialization.Serializable
diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt
new file mode 100644
index 0000000..0a5024d
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt
@@ -0,0 +1,64 @@
+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
new file mode 100644
index 0000000..8927f14
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt
@@ -0,0 +1,12 @@
+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
new file mode 100644
index 0000000..08a8064
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapePathConverter.kt
@@ -0,0 +1,43 @@
+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
new file mode 100644
index 0000000..0174f0f
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt
@@ -0,0 +1,49 @@
+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
new file mode 100644
index 0000000..c48735a
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000..f6b2ef2
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt
@@ -0,0 +1,32 @@
+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
new file mode 100644
index 0000000..88485f4
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt
@@ -0,0 +1,32 @@
+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
new file mode 100644
index 0000000..9778a1a
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/TripDao.kt
@@ -0,0 +1,32 @@
+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
new file mode 100644
index 0000000..b96102e
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/VersionMetadataDao.kt
@@ -0,0 +1,27 @@
+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
new file mode 100644
index 0000000..cc690d6
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/RouteEntity.kt
@@ -0,0 +1,18 @@
+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
new file mode 100644
index 0000000..4b14a95
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt
@@ -0,0 +1,58 @@
+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
new file mode 100644
index 0000000..87ca671
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ShapeEntity.kt
@@ -0,0 +1,19 @@
+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
new file mode 100644
index 0000000..9c6cf15
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt
@@ -0,0 +1,23 @@
+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
new file mode 100644
index 0000000..9b0aac8
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt
@@ -0,0 +1,48 @@
+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
new file mode 100644
index 0000000..ca7e9a7
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt
@@ -0,0 +1,30 @@
+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
new file mode 100644
index 0000000..fc00b44
--- /dev/null
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/VersionMetadataEntity.kt
@@ -0,0 +1,19 @@
+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/core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt
similarity index 53%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt
index f761518..0d6896d 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt
@@ -1,6 +1,5 @@
-package moe.lava.banksia.core.util
+package moe.lava.banksia.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/core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt
similarity index 97%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt
index 22236c6..e41cef6 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.util
+package moe.lava.banksia.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt
similarity index 88%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt
index 9d5f55a..7f26800 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.util
+package moe.lava.banksia.util
fun error(tag: String, throwable: Throwable) = error(tag, "", throwable)
expect fun log(tag: String, msg: String)
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt
similarity index 98%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt
index ec21d62..ee3e826 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.util
+package moe.lava.banksia.util
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt
similarity index 75%
rename from core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt
rename to shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt
index 4db05e2..4aae7d4 100644
--- a/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt
+++ b/shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.util
+package moe.lava.banksia.util
import kotlinx.serialization.Serializable
diff --git a/server/gtfs_rt/src/main/proto/gtfs-realtime.proto b/shared/src/commonMain/proto/gtfs-realtime.proto
similarity index 100%
rename from server/gtfs_rt/src/main/proto/gtfs-realtime.proto
rename to shared/src/commonMain/proto/gtfs-realtime.proto
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
new file mode 100644
index 0000000..d5f83a2
--- /dev/null
+++ b/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt
@@ -0,0 +1,34 @@
+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
new file mode 100644
index 0000000..c24034d
--- /dev/null
+++ b/shared/src/iosMain/kotlin/moe/lava/banksia/util/Logging.ios.kt
@@ -0,0 +1,12 @@
+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
new file mode 100644
index 0000000..3e93241
--- /dev/null
+++ b/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt
@@ -0,0 +1,23 @@
+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/core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt b/shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt
similarity index 86%
rename from core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt
rename to shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt
index de7fdaa..0a1ea10 100644
--- a/core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt
+++ b/shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt
@@ -1,4 +1,4 @@
-package moe.lava.banksia.core.util
+package moe.lava.banksia.util
actual fun log(tag: String, msg: String) {
println("[$tag] $msg")
diff --git a/ui/maps/build.gradle.kts b/ui/maps/build.gradle.kts
deleted file mode 100644
index 4e859d1..0000000
--- a/ui/maps/build.gradle.kts
+++ /dev/null
@@ -1,56 +0,0 @@
-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
deleted file mode 100644
index d76c1f4..0000000
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-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
deleted file mode 100644
index 92a9695..0000000
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-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
deleted file mode 100644
index 94421a7..0000000
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-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
deleted file mode 100644
index b463c18..0000000
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/CameraPosition.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-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
deleted file mode 100644
index 3fe99c2..0000000
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-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
deleted file mode 100644
index ed568c2..0000000
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-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
deleted file mode 100644
index 584c76f..0000000
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-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/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
deleted file mode 100644
index ac33868..0000000
--- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-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/ui/shared/build.gradle.kts b/ui/shared/build.gradle.kts
deleted file mode 100644
index 2a78572..0000000
--- a/ui/shared/build.gradle.kts
+++ /dev/null
@@ -1,55 +0,0 @@
-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/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml
deleted file mode 100644
index ac49572..0000000
--- a/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml
deleted file mode 100644
index 322fa56..0000000
--- a/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/ui/shared/src/commonMain/composeResources/drawable/my_location_24.xml b/ui/shared/src/commonMain/composeResources/drawable/my_location_24.xml
deleted file mode 100644
index e69de29..0000000
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
deleted file mode 100644
index 90914ae..0000000
--- a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-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/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt
deleted file mode 100644
index cff36fb..0000000
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-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/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
deleted file mode 100644
index 9fb37d7..0000000
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-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
deleted file mode 100644
index a1a97d3..0000000
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-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
deleted file mode 100644
index 369721c..0000000
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt
+++ /dev/null
@@ -1,358 +0,0 @@
-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
deleted file mode 100644
index 29bdd37..0000000
--- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-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
- )
- }
- }
-}