Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| b187b63195 |
5
.gitignore
vendored
|
|
@ -18,6 +18,5 @@ captures
|
||||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||||
|
|
||||||
secrets.properties
|
secrets.properties
|
||||||
/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt
|
shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt
|
||||||
/data/
|
data/
|
||||||
/data
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,12 +2,11 @@ plugins {
|
||||||
// this is necessary to avoid the plugins to be loaded multiple times
|
// this is necessary to avoid the plugins to be loaded multiple times
|
||||||
// in each subproject's classloader
|
// in each subproject's classloader
|
||||||
alias(libs.plugins.androidApplication) apply false
|
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.composeMultiplatform) apply false
|
||||||
alias(libs.plugins.composeCompiler) apply false
|
alias(libs.plugins.composeCompiler) apply false
|
||||||
alias(libs.plugins.kotlinJvm) apply false
|
alias(libs.plugins.kotlinJvm) apply false
|
||||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||||
alias(libs.plugins.sqldelight) apply false
|
|
||||||
alias(libs.plugins.wire) apply false
|
alias(libs.plugins.wire) apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,27 @@
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
alias(libs.plugins.androidMultiplatformLibrary)
|
alias(libs.plugins.androidApplication)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
alias(libs.plugins.secretsGradle)
|
alias(libs.plugins.secretsGradle)
|
||||||
|
alias(libs.plugins.spm)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
android {
|
androidTarget {
|
||||||
namespace = "moe.lava.banksia.ui"
|
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
|
||||||
|
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget.set(JvmTarget.JVM_11)
|
jvmTarget.set(JvmTarget.JVM_11)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidResources {
|
|
||||||
enable = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||||
freeCompilerArgs.add("-Xexplicit-backing-fields")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -33,17 +29,26 @@ kotlin {
|
||||||
iosArm64(),
|
iosArm64(),
|
||||||
iosSimulatorArm64()
|
iosSimulatorArm64()
|
||||||
).forEach { iosTarget ->
|
).forEach { iosTarget ->
|
||||||
|
iosTarget.compilations {
|
||||||
|
getByName("main") {
|
||||||
|
cinterops.create("spmMaplibre")
|
||||||
|
}
|
||||||
|
}
|
||||||
iosTarget.binaries.framework {
|
iosTarget.binaries.framework {
|
||||||
baseName = "ComposeApp"
|
baseName = "ComposeApp"
|
||||||
isStatic = true
|
isStatic = true
|
||||||
}
|
}
|
||||||
|
// iosTarget.swiftPackageConfig(cinteropName = "banksia") {
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.compose.ui.tooling.preview)
|
implementation(libs.compose.ui.tooling.preview)
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.play.services.location)
|
implementation(libs.play.services.location)
|
||||||
implementation(projects.ui.shared)
|
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(libs.compose.components.resources)
|
implementation(libs.compose.components.resources)
|
||||||
|
|
@ -67,21 +72,61 @@ kotlin {
|
||||||
implementation(libs.maplibre.compose)
|
implementation(libs.maplibre.compose)
|
||||||
implementation(libs.moko.geo)
|
implementation(libs.moko.geo)
|
||||||
implementation(libs.moko.geo.compose)
|
implementation(libs.moko.geo.compose)
|
||||||
|
implementation(projects.shared)
|
||||||
implementation(libs.ui.backhandler)
|
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 {
|
dependencies {
|
||||||
androidRuntimeClasspath(libs.compose.ui.tooling)
|
debugImplementation(compose.uiTooling)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets {
|
secrets {
|
||||||
propertiesFileName = "secrets.properties"
|
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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="${MAPS_API_KEY}" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#FFFFFFFF" android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -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<Route>()
|
||||||
|
suspend fun getAll() = client.get("routes").body<List<Route>>()
|
||||||
|
}
|
||||||
|
|
@ -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.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.request.get
|
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<Stop>()
|
suspend fun get(id: String) = client.get("stops/${id}").body<Stop>()
|
||||||
suspend fun getByRoute(id: String) = client.get("route_stops/${id}").body<List<Stop>>()
|
suspend fun getByRoute(id: String) = client.get("route_stops/${id}").body<List<Stop>>()
|
||||||
}
|
}
|
||||||
|
|
@ -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.HttpClient
|
||||||
import io.ktor.client.plugins.HttpSend
|
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.client.plugins.plugin
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import moe.lava.banksia.core.Constants
|
import moe.lava.banksia.Constants
|
||||||
import moe.lava.banksia.core.data.repositories.ClientRouteRepository
|
import moe.lava.banksia.client.datasource.local.RouteLocalDataSource
|
||||||
import moe.lava.banksia.core.data.repositories.ClientStopRepository
|
import moe.lava.banksia.client.datasource.local.StopLocalDataSource
|
||||||
import moe.lava.banksia.core.data.repositories.RouteRepository
|
import moe.lava.banksia.client.datasource.remote.RouteRemoteDataSource
|
||||||
import moe.lava.banksia.core.data.repositories.StopRepository
|
import moe.lava.banksia.client.datasource.remote.StopRemoteDataSource
|
||||||
import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
|
import moe.lava.banksia.client.repository.RouteRepository
|
||||||
import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
|
import moe.lava.banksia.client.repository.StopRepository
|
||||||
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
|
|
||||||
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
|
|
||||||
import moe.lava.banksia.core.util.log
|
|
||||||
import moe.lava.banksia.data.ptv.PtvService
|
import moe.lava.banksia.data.ptv.PtvService
|
||||||
|
import moe.lava.banksia.ui.screens.map.MapScreenViewModel
|
||||||
|
import moe.lava.banksia.util.log
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.dsl.bind
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
actual val platformModule = module {
|
val ClientModule = module {
|
||||||
// HTTP Clients
|
// HTTP Clients
|
||||||
singleOf(::PtvService)
|
singleOf(::PtvService)
|
||||||
single {
|
single {
|
||||||
|
|
@ -51,6 +50,9 @@ actual val platformModule = module {
|
||||||
singleOf(::StopRemoteDataSource)
|
singleOf(::StopRemoteDataSource)
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
singleOf(::ClientRouteRepository) bind RouteRepository::class
|
singleOf(::RouteRepository)
|
||||||
singleOf(::ClientStopRepository) bind StopRepository::class
|
singleOf(::StopRepository)
|
||||||
|
|
||||||
|
// ViewModel
|
||||||
|
viewModelOf(::MapScreenViewModel)
|
||||||
}
|
}
|
||||||
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,8 @@ package moe.lava.banksia.ui
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
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 moe.lava.banksia.ui.screens.map.MapScreen
|
||||||
import org.koin.compose.KoinMultiplatformApplication
|
import org.koin.compose.KoinMultiplatformApplication
|
||||||
import org.koin.core.annotation.KoinExperimentalAPI
|
import org.koin.core.annotation.KoinExperimentalAPI
|
||||||
|
|
@ -13,7 +14,7 @@ import org.koin.dsl.koinConfiguration
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App() {
|
||||||
KoinMultiplatformApplication(config = koinConfiguration {
|
KoinMultiplatformApplication(config = koinConfiguration {
|
||||||
modules(AppModule)
|
modules(CommonModules, ClientModule)
|
||||||
}) {
|
}) {
|
||||||
MapScreen()
|
MapScreen()
|
||||||
}
|
}
|
||||||
|
|
@ -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 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.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.Res
|
||||||
import moe.lava.banksia.resources.bus
|
import moe.lava.banksia.resources.bus
|
||||||
import moe.lava.banksia.resources.bus_background
|
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_background
|
||||||
import moe.lava.banksia.resources.tram_icon
|
import moe.lava.banksia.resources.tram_icon
|
||||||
import org.jetbrains.compose.resources.DrawableResource
|
import org.jetbrains.compose.resources.DrawableResource
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
|
||||||
data class RouteTypeProperties(
|
data class RouteTypeProperties(
|
||||||
val colour: Color,
|
val colour: Color,
|
||||||
|
|
@ -29,31 +49,31 @@ const val VLINE_PURPLE = 0xFF8F1A95
|
||||||
|
|
||||||
fun RouteType.getUIProperties(): RouteTypeProperties {
|
fun RouteType.getUIProperties(): RouteTypeProperties {
|
||||||
val colour = when (this) {
|
val colour = when (this) {
|
||||||
RouteType.MetroTrain -> TRAIN_BLUE
|
MetroTrain -> TRAIN_BLUE
|
||||||
RouteType.MetroTram -> TRAM_GREEN
|
MetroTram -> TRAM_GREEN
|
||||||
RouteType.MetroBus -> BUS_ORANGE
|
MetroBus -> BUS_ORANGE
|
||||||
RouteType.RegionalTrain -> VLINE_PURPLE
|
RegionalTrain -> VLINE_PURPLE
|
||||||
RouteType.RegionalCoach -> VLINE_PURPLE
|
RegionalCoach -> VLINE_PURPLE
|
||||||
RouteType.RegionalBus -> VLINE_PURPLE
|
RegionalBus -> VLINE_PURPLE
|
||||||
RouteType.SkyBus -> BUS_ORANGE
|
SkyBus -> BUS_ORANGE
|
||||||
RouteType.Interstate -> BUS_ORANGE
|
Interstate -> BUS_ORANGE
|
||||||
}
|
}
|
||||||
|
|
||||||
val (drawable, background, icon) = when (this) {
|
val (drawable, background, icon) = when (this) {
|
||||||
RouteType.MetroTrain,
|
MetroTrain,
|
||||||
RouteType.RegionalTrain,
|
RegionalTrain,
|
||||||
RouteType.Interstate -> Triple(
|
Interstate -> Triple(
|
||||||
Res.drawable.train, Res.drawable.train_background, Res.drawable.train_icon
|
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
|
Res.drawable.tram, Res.drawable.tram_background, Res.drawable.tram_icon
|
||||||
)
|
)
|
||||||
|
|
||||||
RouteType.MetroBus,
|
MetroBus,
|
||||||
RouteType.RegionalCoach,
|
RegionalCoach,
|
||||||
RouteType.RegionalBus,
|
RegionalBus,
|
||||||
RouteType.SkyBus -> Triple(
|
SkyBus -> Triple(
|
||||||
Res.drawable.bus, Res.drawable.bus_background, Res.drawable.bus_icon
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,16 +35,17 @@ import kotlinx.coroutines.launch
|
||||||
import moe.lava.banksia.resources.Res
|
import moe.lava.banksia.resources.Res
|
||||||
import moe.lava.banksia.resources.my_location_24
|
import moe.lava.banksia.resources.my_location_24
|
||||||
import moe.lava.banksia.ui.layout.AppBottomSheet
|
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.Searcher
|
||||||
import moe.lava.banksia.ui.layout.SheetStateWrapper
|
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.platform.BanksiaTheme
|
||||||
|
import moe.lava.banksia.ui.state.InfoPanelState
|
||||||
|
import moe.lava.banksia.util.Point
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
|
val MELBOURNE = Point(-37.8136, 144.9631)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MapScreen(
|
fun MapScreen(
|
||||||
|
|
@ -65,13 +66,6 @@ fun MapScreen(
|
||||||
val sheetState = SheetStateWrapper.create()
|
val sheetState = SheetStateWrapper.create()
|
||||||
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
|
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val mapsPositionState = rememberMapsPositionState()
|
|
||||||
scope.launch {
|
|
||||||
viewModel.cameraChangeEmitter.collect {
|
|
||||||
mapsPositionState.update(it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(infoState) {
|
LaunchedEffect(infoState) {
|
||||||
if (infoState !is InfoPanelState.None) {
|
if (infoState !is InfoPanelState.None) {
|
||||||
sheetState.peek()
|
sheetState.peek()
|
||||||
|
|
@ -84,21 +78,14 @@ fun MapScreen(
|
||||||
Scaffold {
|
Scaffold {
|
||||||
Maps(
|
Maps(
|
||||||
modifier = Modifier.fillMaxSize(),
|
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()
|
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||||
}, bottom = sheetState.bottomInset),
|
}, bottom = sheetState.bottomInset),
|
||||||
stops = mapState.stops,
|
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||||
positionState = mapsPositionState,
|
|
||||||
// vehicles = mapState.vehicles,
|
|
||||||
onStopClicked = { stop ->
|
|
||||||
viewModel.handleEvent(MapScreenEvent.SelectStop(stop))
|
|
||||||
},
|
|
||||||
// onEvent = viewModel::handleEvent,
|
|
||||||
// cameraPositionFlow = viewModel.cameraChangeEmitter,
|
|
||||||
// setLastKnownLocation = viewModel::setLastKnownLocation,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// onEvent()
|
|
||||||
Searcher(
|
Searcher(
|
||||||
state = searchState,
|
state = searchState,
|
||||||
onEvent = viewModel::handleEvent,
|
onEvent = viewModel::handleEvent,
|
||||||
|
|
@ -13,57 +13,48 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.TimeZone
|
import moe.lava.banksia.client.repository.RouteRepository
|
||||||
import kotlinx.datetime.toInstant
|
import moe.lava.banksia.client.repository.StopRepository
|
||||||
import moe.lava.banksia.core.data.dto.ExtendedStopTime
|
|
||||||
import moe.lava.banksia.core.data.repositories.RouteRepository
|
|
||||||
import moe.lava.banksia.core.data.repositories.StopRepository
|
|
||||||
import moe.lava.banksia.core.data.repositories.StopTimeRepository
|
|
||||||
import moe.lava.banksia.core.model.Route
|
|
||||||
import moe.lava.banksia.core.model.RouteType
|
|
||||||
import moe.lava.banksia.core.util.BoxedValue
|
|
||||||
import moe.lava.banksia.core.util.BoxedValue.Companion.box
|
|
||||||
import moe.lava.banksia.core.util.LoopFlow.Companion.waitUntilSubscribed
|
|
||||||
import moe.lava.banksia.core.util.Point
|
|
||||||
import moe.lava.banksia.core.util.log
|
|
||||||
import moe.lava.banksia.data.ptv.PtvService
|
import moe.lava.banksia.data.ptv.PtvService
|
||||||
import moe.lava.banksia.ui.extensions.getUIProperties
|
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
||||||
import moe.lava.banksia.ui.layout.info.InfoPanelEvent
|
import moe.lava.banksia.model.Route
|
||||||
import moe.lava.banksia.ui.layout.info.InfoPanelState
|
import moe.lava.banksia.model.RouteType
|
||||||
import moe.lava.banksia.ui.layout.info.RouteInfoPanelState
|
import moe.lava.banksia.ui.components.getUIProperties
|
||||||
import moe.lava.banksia.ui.layout.info.StopInfoPanelEvent
|
import moe.lava.banksia.ui.state.InfoPanelState
|
||||||
import moe.lava.banksia.ui.layout.info.StopInfoPanelState
|
|
||||||
import moe.lava.banksia.ui.layout.info.TripInfoPanelState
|
|
||||||
import moe.lava.banksia.ui.map.util.CameraPosition
|
|
||||||
import moe.lava.banksia.ui.map.util.CameraPositionBounds
|
|
||||||
import moe.lava.banksia.ui.map.util.Marker
|
|
||||||
import moe.lava.banksia.ui.state.MapState
|
import moe.lava.banksia.ui.state.MapState
|
||||||
import moe.lava.banksia.ui.state.SearchState
|
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 {
|
sealed class MapScreenEvent {
|
||||||
data object DismissState : MapScreenEvent()
|
data object DismissState : MapScreenEvent()
|
||||||
|
|
||||||
data class SelectRoute(val id: String?) : MapScreenEvent()
|
data class SelectRoute(val id: String?) : MapScreenEvent()
|
||||||
data class SelectRun(val ref: String?) : MapScreenEvent()
|
data class SelectRun(val ref: String?) : MapScreenEvent()
|
||||||
data class SelectStop(val id: String?) : MapScreenEvent()
|
data class SelectStop(val typeIdPair: Pair<RouteType, String>?) : MapScreenEvent()
|
||||||
|
|
||||||
data class SearchUpdate(val text: String) : MapScreenEvent()
|
data class SearchUpdate(val text: String) : MapScreenEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class InternalState(
|
data class InternalState(
|
||||||
val route: String? = null,
|
val route: String? = null,
|
||||||
val stop: String? = null,
|
val stop: Pair<RouteType, String>? = null,
|
||||||
val run: String? = null,
|
val run: String? = null,
|
||||||
|
|
||||||
val lastStopDepartures: List<ExtendedStopTime>? = null,
|
|
||||||
val stopsGrouped: Boolean = true,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class MapScreenViewModel(
|
class MapScreenViewModel(
|
||||||
private val ptvService: PtvService,
|
private val ptvService: PtvService,
|
||||||
private val routeRepository: RouteRepository,
|
private val routeRepository: RouteRepository,
|
||||||
private val stopRepository: StopRepository,
|
private val stopRepository: StopRepository,
|
||||||
private val stopTimeRepository: StopTimeRepository,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private var state = InternalState()
|
private var state = InternalState()
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|
@ -73,10 +64,6 @@ class MapScreenViewModel(
|
||||||
viewModelScope.launch { switchRoute(value.route) }
|
viewModelScope.launch { switchRoute(value.route) }
|
||||||
if (value.stop != last.stop)
|
if (value.stop != last.stop)
|
||||||
viewModelScope.launch { switchStop(value.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)
|
if (value.run != last.run)
|
||||||
switchRun(value.run)
|
switchRun(value.run)
|
||||||
}
|
}
|
||||||
|
|
@ -105,20 +92,12 @@ class MapScreenViewModel(
|
||||||
is MapScreenEvent.DismissState -> dismissState()
|
is MapScreenEvent.DismissState -> dismissState()
|
||||||
is MapScreenEvent.SelectRoute -> state = InternalState(route = event.id)
|
is MapScreenEvent.SelectRoute -> state = InternalState(route = event.id)
|
||||||
is MapScreenEvent.SelectRun -> state = state.copy(run = event.ref, stop = null)
|
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)
|
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) {
|
fun bindTracker(locationTracker: LocationTracker) {
|
||||||
locationTrackerJob = locationTracker.getLocationsFlow()
|
locationTrackerJob = locationTracker.getLocationsFlow()
|
||||||
.onEach { lastKnownLocation = Point(it.latitude, it.longitude) }
|
.onEach { lastKnownLocation = Point(it.latitude, it.longitude) }
|
||||||
|
|
@ -126,6 +105,11 @@ class MapScreenViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun centreCameraToLocation() {
|
fun centreCameraToLocation() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
log("msvm", "getting..")
|
||||||
|
val routes = routeRepository.getAll()
|
||||||
|
log("msvm", routes.joinToString("\n"))
|
||||||
|
}
|
||||||
lastKnownLocation?.let { location ->
|
lastKnownLocation?.let { location ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
log("bvm", "emitting $location")
|
log("bvm", "emitting $location")
|
||||||
|
|
@ -175,9 +159,9 @@ class MapScreenViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
val route = routeRepository.get(routeId)
|
val route = routeRepository.get(routeId)
|
||||||
?: return
|
// val gtfsRoute = ptvService.route(routeId)
|
||||||
iInfoState.update {
|
iInfoState.update {
|
||||||
RouteInfoPanelState(
|
InfoPanelState.Route(
|
||||||
name = route.name,
|
name = route.name,
|
||||||
type = route.type,
|
type = route.type,
|
||||||
)
|
)
|
||||||
|
|
@ -202,7 +186,7 @@ class MapScreenViewModel(
|
||||||
.onEach { run ->
|
.onEach { run ->
|
||||||
if (routeName == null) {
|
if (routeName == null) {
|
||||||
iInfoState.update {
|
iInfoState.update {
|
||||||
TripInfoPanelState(
|
InfoPanelState.Run(
|
||||||
direction = run.destinationName,
|
direction = run.destinationName,
|
||||||
type = RouteType.MetroTrain, // XXX HACK TODO FIXME
|
type = RouteType.MetroTrain, // XXX HACK TODO FIXME
|
||||||
)
|
)
|
||||||
|
|
@ -211,7 +195,7 @@ class MapScreenViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
iInfoState.update {
|
iInfoState.update {
|
||||||
TripInfoPanelState(
|
InfoPanelState.Run(
|
||||||
direction = run.destinationName,
|
direction = run.destinationName,
|
||||||
type = RouteType.MetroTrain, // FIXME HACK XXX TODO
|
type = RouteType.MetroTrain, // FIXME HACK XXX TODO
|
||||||
routeName = routeName,
|
routeName = routeName,
|
||||||
|
|
@ -222,84 +206,65 @@ class MapScreenViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
// [TODO]: Cleanup
|
// [TODO]: Cleanup
|
||||||
private suspend fun switchStop(id: String?) {
|
private suspend fun switchStop(pair: Pair<RouteType, String>?) {
|
||||||
if (id == null) {
|
if (pair == null) {
|
||||||
iInfoState.update { InfoPanelState.None }
|
iInfoState.update { InfoPanelState.None }
|
||||||
state = state.copy(lastStopDepartures = null)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val (type, id) = pair
|
||||||
|
|
||||||
val stop = stopRepository.get(id)
|
val stop = stopRepository.get(id)
|
||||||
|
// val stop = ptvService.stop(routeType, stopId)
|
||||||
val split = stop.name.split("/")
|
val split = stop.name.split("/")
|
||||||
val name = split[0]
|
val name = split[0]
|
||||||
val subname = split.getOrNull(1)
|
val subname = split.getOrNull(1)
|
||||||
iInfoState.update {
|
iInfoState.update {
|
||||||
StopInfoPanelState(
|
InfoPanelState.Stop(
|
||||||
id = stop.id,
|
id = stop.id,
|
||||||
name = name,
|
name = name,
|
||||||
subname = subname,
|
subname = subname,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
stopTimeRepository.getForStop(id)
|
val res = ptvService.departures(type, stop.id)
|
||||||
.onEach { departures ->
|
// Map<
|
||||||
state = state.copy(
|
// Pair<DirectionId, RouteId>,
|
||||||
lastStopDepartures = departures
|
// Pair<DirectionName, List<DepartureTimes>>
|
||||||
)
|
// >
|
||||||
}
|
val timetable = HashMap<Pair<Int, Int>, Pair<String, MutableList<String>>>()
|
||||||
.launchIn(viewModelScope)
|
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) =
|
val date = Instant.parse(dep.estimatedDepartureUtc ?: dep.scheduledDepartureUtc)
|
||||||
platform.takeUnless { it.firstOrNull()?.isDigit() == true }
|
val min = (date - Clock.System.now()).inWholeMinutes
|
||||||
?: "Platform $platform"
|
if (min <= -5)
|
||||||
private fun buildDepartures() {
|
return@forEach
|
||||||
val rawDepartures = state.lastStopDepartures ?: return
|
if (min >= 65)
|
||||||
val departures = if (state.stopsGrouped) {
|
element.add("${((min + 30.0) / 60.0).toInt()}hr")
|
||||||
rawDepartures
|
else
|
||||||
.groupBy { it.stopPlatformCode }
|
element.add("${min}mn")
|
||||||
.mapKeys { (platform) -> platform?.let { friendlyPlatform(it) } }
|
|
||||||
.entries
|
|
||||||
.sortedBy { (platform) -> platform }
|
|
||||||
.map { (platform, deps) ->
|
|
||||||
StopInfoPanelState.DeparturePlatforms(
|
|
||||||
platform = platform ?: "",
|
|
||||||
departures = deps.map {
|
|
||||||
StopInfoPanelState.DepartureInfo(
|
|
||||||
routeName = it.routeNumber ?: it.routeName,
|
|
||||||
routeColour = it.routeType.getUIProperties().colour,
|
|
||||||
headsign = it.headsign ?: it.routeName,
|
|
||||||
description = null,
|
|
||||||
time = it.time.departure.toInstant(TimeZone.currentSystemDefault()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (rawDepartures.isEmpty()) {
|
|
||||||
listOf()
|
|
||||||
} else {
|
|
||||||
listOf(StopInfoPanelState.DeparturePlatforms(platform = "", departures = rawDepartures.map { dep ->
|
|
||||||
StopInfoPanelState.DepartureInfo(
|
|
||||||
routeName = dep.routeNumber ?: dep.routeName,
|
|
||||||
routeColour = dep.routeType.getUIProperties().colour,
|
|
||||||
headsign = dep.headsign ?: dep.routeName,
|
|
||||||
description = dep.stopPlatformCode?.let { friendlyPlatform(it) },
|
|
||||||
time = dep.time.departure.toInstant(TimeZone.currentSystemDefault()),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
val departures = timetable.values.sortedBy { it.first }.map { (name, list) ->
|
||||||
departures.let { departures ->
|
if (list.isEmpty())
|
||||||
iInfoState.update {
|
InfoPanelState.Stop.Departure(name, "No departures")
|
||||||
if (it !is StopInfoPanelState)
|
else
|
||||||
it
|
InfoPanelState.Stop.Departure(name, list.joinToString(" | "))
|
||||||
else
|
}
|
||||||
it.copy(departures = departures)
|
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())
|
val routeWithGeo = if (route.geopath.isEmpty())
|
||||||
ptvService.route(route.routeId, true)
|
ptvService.route(route.routeId, true)
|
||||||
else
|
else
|
||||||
|
|
@ -329,9 +294,9 @@ class MapScreenViewModel(
|
||||||
|
|
||||||
iMapState.update { it.copy(polylines = polylines) }
|
iMapState.update { it.copy(polylines = polylines) }
|
||||||
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
||||||
}*/
|
}
|
||||||
|
|
||||||
/*private fun buildRuns(route: PtvRoute) {
|
private fun buildRuns(route: PtvRoute) {
|
||||||
ptvService
|
ptvService
|
||||||
.runsFlow(route.routeId)
|
.runsFlow(route.routeId)
|
||||||
.waitUntilSubscribed(iInfoState)
|
.waitUntilSubscribed(iInfoState)
|
||||||
|
|
@ -352,16 +317,19 @@ class MapScreenViewModel(
|
||||||
iMapState.update { it.copy(vehicles = markers) }
|
iMapState.update { it.copy(vehicles = markers) }
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}*/
|
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun buildStops(route: Route) {
|
private suspend fun buildStops(route: Route) {
|
||||||
val stops = stopRepository.getByRoute(route.id)
|
val stops = stopRepository.getByRoute(route.id)
|
||||||
|
val colour = route.type.getUIProperties().colour
|
||||||
|
|
||||||
val markers = stops
|
val markers = stops
|
||||||
.map { stop ->
|
.map { stop ->
|
||||||
Marker.Stop(
|
Marker.Stop(
|
||||||
point = stop.pos,
|
point = stop.pos,
|
||||||
id = stop.id,
|
id = stop.id,
|
||||||
|
colour = colour,
|
||||||
type = route.type,
|
type = route.type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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<Marker>): FeatureCollection<MLPoint, MarkerProps> {
|
||||||
|
return buildFeatureCollection {
|
||||||
|
markers.forEach { marker ->
|
||||||
|
val type = when (marker) {
|
||||||
|
is Marker.Stop -> marker.type
|
||||||
|
is Marker.Vehicle -> marker.type
|
||||||
|
}
|
||||||
|
val id = when (marker) {
|
||||||
|
is Marker.Stop -> marker.id
|
||||||
|
is Marker.Vehicle -> marker.ref
|
||||||
|
}
|
||||||
|
addFeature(
|
||||||
|
geometry = MLPoint(marker.point.toPos()),
|
||||||
|
properties = MarkerProps(type),
|
||||||
|
) {
|
||||||
|
setId(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val colorTypeExpression @Composable get() = switch(
|
||||||
|
input = feature["type"].convertToString(),
|
||||||
|
cases = RouteType.entries.map {
|
||||||
|
case(label = it.name, output = const(it.getUIProperties().colour))
|
||||||
|
}.toTypedArray(),
|
||||||
|
fallback = const(BanksiaTheme.colors.surface),
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Maps(
|
||||||
|
modifier: Modifier,
|
||||||
|
state: MapState,
|
||||||
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
|
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||||
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
|
extInsets: WindowInsets,
|
||||||
|
) {
|
||||||
|
val camPos = rememberCameraState(
|
||||||
|
MLCameraPosition(
|
||||||
|
zoom = 16.0,
|
||||||
|
target = MELBOURNE.toPos()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
|
||||||
|
LaunchedEffect(newCameraPos) {
|
||||||
|
log("maps", "newPos ${newCameraPos?.value}")
|
||||||
|
val pos = newCameraPos?.value ?: return@LaunchedEffect
|
||||||
|
if (pos.bounds != null) {
|
||||||
|
val (northeast, southwest) = pos.bounds
|
||||||
|
camPos.animateTo(
|
||||||
|
boundingBox = BoundingBox(
|
||||||
|
southwest.toPos(),
|
||||||
|
northeast.toPos()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
camPos.animateTo(MLCameraPosition(
|
||||||
|
target = pos.centre.toPos(),
|
||||||
|
zoom = 16.0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// val ctx = LocalContext.current
|
||||||
|
// val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) }
|
||||||
|
// LaunchedEffect(Unit) {
|
||||||
|
// @SuppressLint("MissingPermission")
|
||||||
|
// fusedLocation.lastLocation.addOnSuccessListener {
|
||||||
|
// if (it != null) {
|
||||||
|
// camPos.position = MLCameraPosition(
|
||||||
|
// zoom = 16.0,
|
||||||
|
// target = Position(it.longitude, it.latitude)
|
||||||
|
// )
|
||||||
|
// setLastKnownLocation(Point(it.latitude, it.longitude))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
MaplibreMap(
|
||||||
|
modifier = modifier,
|
||||||
|
baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/positron"),
|
||||||
|
cameraState = camPos,
|
||||||
|
options = MapOptions(
|
||||||
|
ornamentOptions = OrnamentOptions(
|
||||||
|
padding = WindowInsets.safeDrawing.add(extInsets).asPaddingValues(),
|
||||||
|
isScaleBarEnabled = false,
|
||||||
|
isAttributionEnabled = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (state.stops.isNotEmpty()) {
|
||||||
|
val stopsSource = rememberGeoJsonSource(
|
||||||
|
GeoJsonData.Features(buildMarkers(state.stops))
|
||||||
|
)
|
||||||
|
CircleLayer(
|
||||||
|
id = "maps-stops0",
|
||||||
|
source = stopsSource,
|
||||||
|
color = const(BanksiaTheme.colors.surface),
|
||||||
|
radius = const(3.dp),
|
||||||
|
strokeWidth = const(2.dp),
|
||||||
|
strokeColor = colorTypeExpression,
|
||||||
|
)
|
||||||
|
CircleLayer(
|
||||||
|
id = "maps-stops0-clickhandler",
|
||||||
|
source = stopsSource,
|
||||||
|
color = const(Color.Transparent),
|
||||||
|
radius = const(12.dp),
|
||||||
|
onClick = { features ->
|
||||||
|
val feature = features[0]
|
||||||
|
val marker = Json.decodeFromJsonElement<MarkerProps>(feature.properties!!)
|
||||||
|
onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content))
|
||||||
|
ClickResult.Consume
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// if (state.vehicles.isNotEmpty()) {
|
||||||
|
// val stopsSource = rememberGeoJsonSource(
|
||||||
|
// GeoJsonData.Features(buildMarkers(state.vehicles))
|
||||||
|
// )
|
||||||
|
// SymbolLayer
|
||||||
|
// CircleLayer(
|
||||||
|
// id = "maps-vehicles0",
|
||||||
|
// source = stopsSource,
|
||||||
|
// color = const(BanksiaTheme.colors.surface),
|
||||||
|
// radius = const(3.dp),
|
||||||
|
// strokeWidth = const(2.dp),
|
||||||
|
// strokeColor = colorTypeExpression,
|
||||||
|
// onClick = { features ->
|
||||||
|
// val feature = features[0]
|
||||||
|
// val marker = Json.decodeFromJsonElement<MarkerProps>(feature.properties!!)
|
||||||
|
// onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content))
|
||||||
|
// ClickResult.Consume
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (state.polylines.isNotEmpty()) {
|
||||||
|
// val polySource = rememberGeoJsonSource(
|
||||||
|
//
|
||||||
|
// )
|
||||||
|
// LineLayer(
|
||||||
|
// id = "maps-routeline",
|
||||||
|
// source = polySource,
|
||||||
|
// color = colorTypeExpression,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Departure>? = null,
|
||||||
|
) : InfoPanelState() {
|
||||||
|
override val loading: Boolean
|
||||||
|
get() = departures == null
|
||||||
|
|
||||||
|
data class Departure(val directionName: String, val formattedTimes: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package moe.lava.banksia.ui.state
|
package moe.lava.banksia.ui.state
|
||||||
|
|
||||||
import moe.lava.banksia.ui.map.util.Marker
|
import moe.lava.banksia.ui.utils.map.Marker
|
||||||
import moe.lava.banksia.ui.map.util.Polyline
|
import moe.lava.banksia.ui.utils.map.Polyline
|
||||||
|
|
||||||
data class MapState(
|
data class MapState(
|
||||||
val stops: List<Marker.Stop> = listOf(),
|
val stops: List<Marker.Stop> = listOf(),
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package moe.lava.banksia.ui.state
|
package moe.lava.banksia.ui.state
|
||||||
|
|
||||||
import moe.lava.banksia.core.model.RouteType
|
import moe.lava.banksia.model.RouteType
|
||||||
|
|
||||||
data class SearchState(
|
data class SearchState(
|
||||||
val entries: List<SearchEntry> = listOf(),
|
val entries: List<SearchEntry> = listOf(),
|
||||||
|
|
@ -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(
|
data class CameraPosition(
|
||||||
val centre: Point = Point(-37.8136, 144.9631),
|
val centre: Point = Point(-37.8136, 144.9631),
|
||||||
|
|
@ -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)
|
data class CameraPositionBounds(val northeast: Point, val southwest: Point)
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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 androidx.compose.ui.graphics.Color
|
||||||
import moe.lava.banksia.core.util.Point
|
import moe.lava.banksia.util.Point
|
||||||
|
|
||||||
data class Polyline(val points: List<Point>, val colour: Color)
|
data class Polyline(val points: List<Point>, val colour: Color)
|
||||||
15
composeApp/src/swift/spmMaplibre/StartYourBridgeHere.swift
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
**/
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<Long, Route>()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<Route>()
|
|
||||||
suspend fun getByPattern(patternId: Long) = client.get("routes/by_pattern/${patternId}").body<Route>()
|
|
||||||
suspend fun getAll() = client.get("routes").body<List<Route>>()
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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<Route>
|
|
||||||
}
|
|
||||||
|
|
@ -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<Stop>
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package moe.lava.banksia.core.data
|
|
||||||
|
|
||||||
import org.koin.dsl.module
|
|
||||||
|
|
||||||
internal actual val platformModule = module {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<Context>().applicationContext
|
|
||||||
val driver = AndroidSqliteDriver(BanksiaDatabase.Schema, ctx, "${DBNAME}.db")
|
|
||||||
BanksiaDatabase(driver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package moe.lava.banksia.core.sqld
|
|
||||||
|
|
||||||
internal const val DBNAME = "timetable"
|
|
||||||
|
|
||||||
expect class DatabaseManager() {
|
|
||||||
val database: BanksiaDatabase
|
|
||||||
}
|
|
||||||
|
|
@ -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<DatabaseManager>().database }
|
|
||||||
factory { get<BanksiaDatabase>().routeQueries }
|
|
||||||
factory { get<BanksiaDatabase>().serviceQueries }
|
|
||||||
factory { get<BanksiaDatabase>().serviceExceptionQueries }
|
|
||||||
factory { get<BanksiaDatabase>().shapeQueries }
|
|
||||||
factory { get<BanksiaDatabase>().stopQueries }
|
|
||||||
factory { get<BanksiaDatabase>().stoppingPatternQueries }
|
|
||||||
factory { get<BanksiaDatabase>().stopTimeQueries }
|
|
||||||
factory { get<BanksiaDatabase>().tripQueries }
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
|
|
@ -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 <T: TimeType> DbStoppingPattern.asModel(stoptimes: List<StopTime<T>>) = 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,
|
|
||||||
)
|
|
||||||
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
|
|
@ -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 ?;
|
|
||||||
|
|
@ -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 ?;
|
|
||||||
|
|
@ -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 ?;
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
CREATE TABLE Shape (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
|
||||||
path BLOB NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
insert:
|
|
||||||
INSERT INTO Shape VALUES ?;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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 ?;
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
package moe.lava.banksia.core.endpoints
|
|
||||||
|
|
||||||
object Endpoint
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
@ -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<T: TimeType>(
|
|
||||||
val patternId: Long,
|
|
||||||
val stopId: String,
|
|
||||||
val time: T,
|
|
||||||
val pickupType: Int,
|
|
||||||
val dropOffType: Int,
|
|
||||||
) {
|
|
||||||
typealias Dated = StopTime<TimeType.Dated>
|
|
||||||
typealias Undated = StopTime<TimeType.Undated>
|
|
||||||
}
|
|
||||||
|
|
||||||
@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<TimeType.Undated>.atDate(date: LocalDate) = StopTime(
|
|
||||||
patternId = patternId,
|
|
||||||
stopId = stopId,
|
|
||||||
time = time.atDate(date),
|
|
||||||
pickupType = pickupType,
|
|
||||||
dropOffType = dropOffType,
|
|
||||||
)
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package moe.lava.banksia.core.model
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StoppingPattern<T: TimeType>(
|
|
||||||
val id: Long,
|
|
||||||
val routeId: String,
|
|
||||||
val shapeId: String,
|
|
||||||
val headsign: String,
|
|
||||||
val wheelchairAccessible: Boolean,
|
|
||||||
val stoptimes: List<StopTime<T>>,
|
|
||||||
) {
|
|
||||||
typealias Dated = StoppingPattern<TimeType.Dated>
|
|
||||||
typealias Undated = StoppingPattern<TimeType.Undated>
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package moe.lava.banksia.core.model
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Trip<T: TimeType>(
|
|
||||||
val id: String,
|
|
||||||
val pattern: StoppingPattern<T>,
|
|
||||||
val service: Service,
|
|
||||||
val directionId: Int,
|
|
||||||
val blockId: String?,
|
|
||||||
) {
|
|
||||||
typealias Dated = Trip<TimeType.Dated>
|
|
||||||
typealias Undated = Trip<TimeType.Undated>
|
|
||||||
}
|
|
||||||
|
|
@ -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<DayOfWeek> = 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<DayOfWeek>.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||