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 - ) - } - } -}