Compare commits

...
Sign in to create a new pull request.

33 commits

Author SHA1 Message Date
8b3016004b
feat: basic departures support
also a huge refactor to simplify modules
2026-06-23 00:07:10 +10:00
b31067992d
feat(server/gtfsr): compress after every day 2026-05-27 16:35:23 +10:00
102c028407
refactor: optimisation around stoptimes
- moved stoptime related functionality into new core:data:stoptime module
  - will feature all the different realtime stoptime sources to be
      integrated later
- create proper database schema for future migrations
- deduplicate trips into stoppingpatterns, since many trips share the
  exact same stopping pattern
  - stoptimes are now linked to stoppingpatterns instead
  - stoppingpattern ids are generated from a hash composed of all stoptimes
- stoptimes now use deltas for arrival time to save space
2026-05-05 03:23:11 +10:00
f1770744db
refactor(core): switch from room to sqldelight
sqldelight provides far more control over the sql and allows me to make
more optimisations such as removing generated rowid etc. sql also just
looks better than the annotation hell from room.
2026-05-02 02:31:18 +10:00
ff2af310fb
feat(server): use envvar for devmode constant 2026-04-30 17:26:41 +10:00
ef630b6d58
fix(server/gtfs): resolve duplicate stops more intelligently 2026-04-12 20:43:45 +10:00
415ce8d88f
fix(core/room): use null instead of empty parent 2026-04-12 01:02:53 +10:00
29a804b0fb
fix(core/room/server): ignore non-existing files in file operations 2026-04-12 00:32:28 +10:00
27f2a08d77
feat(server): add fixup endpoint and move update endpoint 2026-04-12 00:31:41 +10:00
38bcdc54bc
fix(server/gtfs): null empty parents 2026-04-11 23:34:47 +10:00
0524eda5d2
feat(server): use lazy swappable database 2026-04-11 21:59:26 +10:00
959b022cf9
feat(core/room): swappable databases 2026-04-11 21:33:33 +10:00
e7caeca356
refactor: core:data -> core:data:client 2026-04-02 21:55:45 +11:00
4cdb9a305c
fix(ui): make setting camera position work again 2026-04-02 02:28:10 +11:00
c912723c78
refactor: shared -> core 2026-04-02 01:57:08 +11:00
104a77b27e
refactor: split up room into a module, and move client module 2026-04-01 22:48:04 +11:00
c55e3a3232
feat(server): better support for parent stops
- add datafixer to add parent stops for likely candidates
  - this is mainly for bus hubs, the heuristic is the existence of a
    platform code and missing parent
- use parent stops as default in route_stops
- support parent stops for stoptime querying
2026-04-01 20:37:58 +11:00
58649b6171
feat(server/gtfs): service exception support 2026-04-01 19:31:31 +11:00
c9aeeb99c1
fix(server/gtfs): chunk stop times into smaller blocks 2026-04-01 17:23:59 +11:00
91d4fe25a6
feat(server/gtfs): use transaction for imports 2026-04-01 17:00:35 +11:00
b568ed547a
chore: bump and cleanup dependencies 2026-04-01 16:43:02 +11:00
ed9d294afc
refactor(server): move gtfsrt to separate module 2026-04-01 16:32:01 +11:00
0181497420
refactor(server): split gtfs into its own module 2026-03-31 23:12:54 +11:00
aad5ae4024
refactor(ui/info): split up info panel state 2026-03-31 20:53:21 +11:00
f0ce780bba
refactor(ui): split up infopanel into files 2026-03-31 20:21:39 +11:00
72b9fb2757
feat: stop times/departures reimpl based on gtfs 2026-03-31 20:09:48 +11:00
b5f2ec102d
fix(room): make stoptime's fkids non-unique
ajsodfijasfoiasjgifasngiuash
2026-03-28 22:13:37 +11:00
8925c943ab
feat(ui/map): switch to protomaps
has POIs and also just looks a bit prettier :3
2026-03-28 21:22:48 +11:00
a79c95829e
refactor(ui): split into shared, maps, and main modules 2026-03-26 02:55:46 +11:00
aab03ced07
fix(shared/room): add indices according to warnings 2026-03-25 21:53:13 +11:00
758f9cea01
refactor: use gradle toolchain 2026-03-25 21:14:28 +11:00
2b64fdcda9
refactor: migrate to agp9 2026-03-25 21:10:54 +11:00
74338d6dce
refactor: split composeApp to client and ui
also renamed client.datasource to client.data, which made me realise
.gitignore was ignoring `data` and therefore some gtfsr source files
😭
2026-03-02 00:09:33 +11:00
208 changed files with 3465 additions and 2790 deletions

5
.gitignore vendored
View file

@ -18,5 +18,6 @@ captures
**/xcshareddata/WorkspaceSettings.xcsettings **/xcshareddata/WorkspaceSettings.xcsettings
secrets.properties secrets.properties
shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt /core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt
data/ /data/
/data

View file

@ -0,0 +1,57 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
target {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
compilerOptions {
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
dependencies {
implementation(projects.ui)
implementation(libs.androidx.activity.compose)
implementation(libs.compose.ui.tooling.preview)
}
}
dependencies {
debugImplementation(libs.compose.ui.tooling)
}
android {
namespace = "moe.lava.banksia"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
applicationId = "moe.lava.banksia"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View file

@ -13,9 +13,6 @@
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"

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -2,11 +2,12 @@ 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.androidLibrary) apply false alias(libs.plugins.androidMultiplatformLibrary) 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
} }

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View file

@ -1,11 +0,0 @@
package moe.lava.banksia.client.datasource.local
import moe.lava.banksia.model.Route
import moe.lava.banksia.room.dao.RouteDao
import moe.lava.banksia.room.entity.asEntity
class RouteLocalDataSource(private val dao: RouteDao) {
suspend fun get(id: String) = dao.get(id)
suspend fun getAll() = dao.getAll()
suspend fun save(vararg routes: Route) = dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray())
}

View file

@ -1,12 +0,0 @@
package moe.lava.banksia.client.datasource.local
import moe.lava.banksia.model.Stop
import moe.lava.banksia.room.dao.RouteDao
import moe.lava.banksia.room.dao.StopDao
import moe.lava.banksia.room.entity.asEntity
class StopLocalDataSource(private val dao: StopDao, private val routeDao: RouteDao) {
suspend fun get(id: String) = dao.get(id)
suspend fun getByRoute(id: String) = routeDao.stops(id)
suspend fun save(vararg stops: Stop) = dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray())
}

View file

@ -1,11 +0,0 @@
package moe.lava.banksia.client.datasource.remote
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import moe.lava.banksia.model.Route
class RouteRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("routes/${id}").body<Route>()
suspend fun getAll() = client.get("routes").body<List<Route>>()
}

View file

@ -1,25 +0,0 @@
package moe.lava.banksia.client.repository
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.client.datasource.local.RouteLocalDataSource
import moe.lava.banksia.client.datasource.remote.RouteRemoteDataSource
class RouteRepository(
private val local: RouteLocalDataSource,
private val remote: RouteRemoteDataSource,
) {
private val mutex = Mutex()
suspend fun getAll() = mutex.withLock {
local
.getAll()
.map { it.asModel() }
.ifEmpty {
remote
.getAll()
.also { local.save(*it.toTypedArray()) }
}
}
suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
}

View file

@ -1,22 +0,0 @@
package moe.lava.banksia.client.repository
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.client.datasource.local.StopLocalDataSource
import moe.lava.banksia.client.datasource.remote.StopRemoteDataSource
class StopRepository(
private val local: StopLocalDataSource,
private val remote: StopRemoteDataSource,
) {
private val mutex = Mutex()
suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
suspend fun getByRoute(id: String) = mutex.withLock {
local
.getByRoute(id)
.map { it.asModel() }
.ifEmpty { null }
?: remote.getByRoute(id)
}
}

View file

@ -1,177 +0,0 @@
package moe.lava.banksia.ui.layout
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import moe.lava.banksia.ui.components.RouteIcon
import moe.lava.banksia.ui.screens.map.MapScreenEvent
import moe.lava.banksia.ui.state.InfoPanelState
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun InfoPanel(
state: InfoPanelState,
onEvent: (MapScreenEvent) -> Unit,
onPeekHeightChange: (Dp) -> Unit,
) {
if (state is InfoPanelState.None)
return
val localDensity = LocalDensity.current
var delayedLoad by remember { mutableStateOf(false) }
LaunchedEffect(state.loading) {
if (state.loading) {
delay(200.milliseconds)
delayedLoad = true
} else {
delayedLoad = false
}
}
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.onSizeChanged {
onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
}
) {
Box {
when (state) {
is InfoPanelState.Route -> RouteInfoPanel(state, onEvent)
is InfoPanelState.Stop -> StopInfoPanel(state, onEvent)
is InfoPanelState.Run -> RunInfoPanel(state, onEvent)
is InfoPanelState.None -> throw UnsupportedOperationException()
}
this@Column.AnimatedVisibility(
modifier = Modifier.align(Alignment.TopEnd),
visible = delayedLoad,
label = "sheet-loading",
enter = fadeIn() + scaleIn(),
exit = fadeOut() + scaleOut(),
) {
LoadingIndicator(
modifier = Modifier.size(48.dp)
)
}
}
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent))
}
}
@Composable
private inline fun RouteInfoPanel(
state: InfoPanelState.Route,
onEvent: (MapScreenEvent) -> Unit,
) {
Column(Modifier.fillMaxWidth()) {
Row {
RouteIcon(routeType = state.type)
Text(
state.name,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Start
)
}
}
}
@Composable
private inline fun RunInfoPanel(
state: InfoPanelState.Run,
onEvent: (MapScreenEvent) -> Unit,
) {
Column(Modifier.fillMaxWidth()) {
Row {
RouteIcon(routeType = state.type)
Text(
"${state.direction} via ${state.routeName ?: "..."}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Start
)
}
}
}
@Composable
private inline fun StopInfoPanel(
state: InfoPanelState.Stop,
onEvent: (MapScreenEvent) -> Unit,
) {
Column(Modifier.fillMaxWidth()) {
Text(
state.name,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Start
)
state.subname?.let {
Text(
"/ $it",
modifier = Modifier.padding(start = 5.dp),
style = MaterialTheme.typography.titleSmall,
color = Color.Gray,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Start
)
}
state.departures?.let {
Spacer(Modifier.height(5.dp))
it.forEach { (name, formatted) ->
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
formatted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 5.dp)
)
}
}
}
}
}

View file

@ -1,210 +0,0 @@
@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
package moe.lava.banksia.ui.screens.map
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import moe.lava.banksia.model.RouteType
import moe.lava.banksia.ui.components.getUIProperties
import moe.lava.banksia.ui.platform.BanksiaTheme
import moe.lava.banksia.ui.state.MapState
import moe.lava.banksia.ui.utils.map.CameraPosition
import moe.lava.banksia.ui.utils.map.Marker
import moe.lava.banksia.util.BoxedValue
import moe.lava.banksia.util.Point
import moe.lava.banksia.util.log
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.dsl.case
import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.expressions.dsl.convertToString
import org.maplibre.compose.expressions.dsl.feature
import org.maplibre.compose.expressions.dsl.switch
import org.maplibre.compose.layers.CircleLayer
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.GeoJsonData
import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.compose.util.ClickResult
import org.maplibre.spatialk.geojson.BoundingBox
import org.maplibre.spatialk.geojson.FeatureCollection
import org.maplibre.spatialk.geojson.Position
import org.maplibre.spatialk.geojson.dsl.addFeature
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
import org.maplibre.compose.camera.CameraPosition as MLCameraPosition
import org.maplibre.spatialk.geojson.Point as MLPoint
fun Point.toPos(): Position = Position(this.lng, this.lat)
@Serializable
data class MarkerProps(
val type: RouteType,
)
private fun buildMarkers(markers: List<Marker>): FeatureCollection<MLPoint, MarkerProps> {
return buildFeatureCollection {
markers.forEach { marker ->
val type = when (marker) {
is Marker.Stop -> marker.type
is Marker.Vehicle -> marker.type
}
val id = when (marker) {
is Marker.Stop -> marker.id
is Marker.Vehicle -> marker.ref
}
addFeature(
geometry = MLPoint(marker.point.toPos()),
properties = MarkerProps(type),
) {
setId(id)
}
}
}
}
private val colorTypeExpression @Composable get() = switch(
input = feature["type"].convertToString(),
cases = RouteType.entries.map {
case(label = it.name, output = const(it.getUIProperties().colour))
}.toTypedArray(),
fallback = const(BanksiaTheme.colors.surface),
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Maps(
modifier: Modifier,
state: MapState,
onEvent: (MapScreenEvent) -> Unit,
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
setLastKnownLocation: (Point) -> Unit,
extInsets: WindowInsets,
) {
val camPos = rememberCameraState(
MLCameraPosition(
zoom = 16.0,
target = MELBOURNE.toPos()
)
)
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
LaunchedEffect(newCameraPos) {
log("maps", "newPos ${newCameraPos?.value}")
val pos = newCameraPos?.value ?: return@LaunchedEffect
if (pos.bounds != null) {
val (northeast, southwest) = pos.bounds
camPos.animateTo(
boundingBox = BoundingBox(
southwest.toPos(),
northeast.toPos()
)
)
} else {
camPos.animateTo(MLCameraPosition(
target = pos.centre.toPos(),
zoom = 16.0,
))
}
}
//
// val ctx = LocalContext.current
// val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) }
// LaunchedEffect(Unit) {
// @SuppressLint("MissingPermission")
// fusedLocation.lastLocation.addOnSuccessListener {
// if (it != null) {
// camPos.position = MLCameraPosition(
// zoom = 16.0,
// target = Position(it.longitude, it.latitude)
// )
// setLastKnownLocation(Point(it.latitude, it.longitude))
// }
// }
// }
MaplibreMap(
modifier = modifier,
baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/positron"),
cameraState = camPos,
options = MapOptions(
ornamentOptions = OrnamentOptions(
padding = WindowInsets.safeDrawing.add(extInsets).asPaddingValues(),
isScaleBarEnabled = false,
isAttributionEnabled = false,
)
)
) {
if (state.stops.isNotEmpty()) {
val stopsSource = rememberGeoJsonSource(
GeoJsonData.Features(buildMarkers(state.stops))
)
CircleLayer(
id = "maps-stops0",
source = stopsSource,
color = const(BanksiaTheme.colors.surface),
radius = const(3.dp),
strokeWidth = const(2.dp),
strokeColor = colorTypeExpression,
)
CircleLayer(
id = "maps-stops0-clickhandler",
source = stopsSource,
color = const(Color.Transparent),
radius = const(12.dp),
onClick = { features ->
val feature = features[0]
val marker = Json.decodeFromJsonElement<MarkerProps>(feature.properties!!)
onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content))
ClickResult.Consume
}
)
}
// TODO
// if (state.vehicles.isNotEmpty()) {
// val stopsSource = rememberGeoJsonSource(
// GeoJsonData.Features(buildMarkers(state.vehicles))
// )
// SymbolLayer
// CircleLayer(
// id = "maps-vehicles0",
// source = stopsSource,
// color = const(BanksiaTheme.colors.surface),
// radius = const(3.dp),
// strokeWidth = const(2.dp),
// strokeColor = colorTypeExpression,
// onClick = { features ->
// val feature = features[0]
// val marker = Json.decodeFromJsonElement<MarkerProps>(feature.properties!!)
// onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content))
// ClickResult.Consume
// }
// )
// }
//
// if (state.polylines.isNotEmpty()) {
// val polySource = rememberGeoJsonSource(
//
// )
// LineLayer(
// id = "maps-routeline",
// source = polySource,
// color = colorTypeExpression,
// )
// }
}
}

View file

@ -1,38 +0,0 @@
package moe.lava.banksia.ui.state
import moe.lava.banksia.model.RouteType
sealed class InfoPanelState {
abstract val loading: Boolean
data object None : InfoPanelState() {
override val loading = false
}
data class Route(
val name: String,
val type: RouteType,
) : InfoPanelState() {
override val loading = false
}
data class Run(
val direction: String,
val type: RouteType,
val routeName: String? = null,
) : InfoPanelState() {
override val loading = routeName == null
}
data class Stop(
val id: String,
val name: String,
val subname: String? = null,
val departures: List<Departure>? = null,
) : InfoPanelState() {
override val loading: Boolean
get() = departures == null
data class Departure(val directionName: String, val formattedTimes: String)
}
}

View file

@ -1,22 +0,0 @@
package moe.lava.banksia.ui.utils.map
import androidx.compose.ui.graphics.Color
import moe.lava.banksia.model.RouteType
import moe.lava.banksia.util.Point
sealed class Marker {
abstract val point: Point
data class Stop(
override val point: Point,
val id: String,
val type: RouteType,
val colour: Color,
) : Marker()
data class Vehicle(
override val point: Point,
val ref: String,
val type: RouteType,
) : Marker()
}

View file

@ -1,22 +1,16 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization) alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidLibrary) alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.ksp)
alias(libs.plugins.room)
alias(libs.plugins.wire)
}
room {
schemaDirectory("$projectDir/schemas")
} }
kotlin { kotlin {
androidTarget { android {
@OptIn(ExperimentalKotlinGradlePluginApi::class) namespace = "moe.lava.banksia.core"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_11) jvmTarget.set(JvmTarget.JVM_11)
} }
@ -26,7 +20,6 @@ kotlin {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
} }
iosX64()
iosArm64() iosArm64()
iosSimulatorArm64() iosSimulatorArm64()
@ -47,38 +40,9 @@ kotlin {
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf) implementation(libs.kotlinx.serialization.protobuf)
implementation(libs.room.runtime)
implementation(libs.sqlite.bundled)
} }
iosMain.dependencies { iosMain.dependencies {
implementation(libs.ktor.client.darwin) implementation(libs.ktor.client.darwin)
} }
} }
} }
dependencies {
add("kspAndroid", libs.room.compiler)
add("kspIosX64", libs.room.compiler)
add("kspIosArm64", libs.room.compiler)
add("kspIosSimulatorArm64", libs.room.compiler)
add("kspJvm", libs.room.compiler)
}
android {
namespace = "moe.lava.banksia.shared"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
}
wire {
sourcePath {
srcDir("src/commonMain/proto")
}
kotlin {}
}

View file

@ -0,0 +1,64 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidMultiplatformLibrary)
}
kotlin {
android {
namespace = "moe.lava.banksia.core.data"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
val clientMain by creating {
dependsOn(commonMain.get())
}
androidMain.get().dependsOn(clientMain)
iosArm64Main.get().dependsOn(clientMain)
iosSimulatorArm64Main.get().dependsOn(clientMain)
commonMain.dependencies {
implementation(libs.koin.core)
implementation(projects.core)
api(projects.core.stoptime)
}
androidMain.dependencies {
implementation(libs.koin.compose)
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
implementation(projects.core)
implementation(projects.core.sqld)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.client.di package moe.lava.banksia.core.data
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpSend import io.ktor.client.plugins.HttpSend
@ -7,21 +7,22 @@ import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.plugin import io.ktor.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.Constants import moe.lava.banksia.core.Constants
import moe.lava.banksia.client.datasource.local.RouteLocalDataSource import moe.lava.banksia.core.data.repositories.ClientRouteRepository
import moe.lava.banksia.client.datasource.local.StopLocalDataSource import moe.lava.banksia.core.data.repositories.ClientStopRepository
import moe.lava.banksia.client.datasource.remote.RouteRemoteDataSource import moe.lava.banksia.core.data.repositories.RouteRepository
import moe.lava.banksia.client.datasource.remote.StopRemoteDataSource import moe.lava.banksia.core.data.repositories.StopRepository
import moe.lava.banksia.client.repository.RouteRepository import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
import moe.lava.banksia.client.repository.StopRepository import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
import moe.lava.banksia.core.util.log
import moe.lava.banksia.data.ptv.PtvService import moe.lava.banksia.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.core.module.dsl.viewModelOf import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
val ClientModule = module { actual val platformModule = module {
// HTTP Clients // HTTP Clients
singleOf(::PtvService) singleOf(::PtvService)
single { single {
@ -50,9 +51,6 @@ val ClientModule = module {
singleOf(::StopRemoteDataSource) singleOf(::StopRemoteDataSource)
// Repositories // Repositories
singleOf(::RouteRepository) singleOf(::ClientRouteRepository) bind RouteRepository::class
singleOf(::StopRepository) singleOf(::ClientStopRepository) bind StopRepository::class
// ViewModel
viewModelOf(::MapScreenViewModel)
} }

View file

@ -0,0 +1,36 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.sqld.mappers.asModel
internal class ClientRouteRepository internal constructor(
private val local: RouteLocalDataSource,
private val remote: RouteRemoteDataSource,
) : RouteRepository {
private val mutex = Mutex()
override suspend fun getAll() = mutex.withLock {
local
.getAll()
.map { it.asModel() }
.ifEmpty {
remote
.getAll()
.also { local.save(*it.toTypedArray()) }
}
}
private val tripRouteMap = mutableMapOf<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
}
}
}

View file

@ -0,0 +1,23 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
import moe.lava.banksia.core.sqld.mappers.asModel
internal class ClientStopRepository internal constructor(
private val local: StopLocalDataSource,
private val remote: StopRemoteDataSource,
) : StopRepository {
private val mutex = Mutex()
override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
override suspend fun getByRoute(id: String) = mutex.withLock {
local
.getByRoute(id)
.map { it.asModel() }
.ifEmpty { null }
?: remote.getByRoute(id)
}
}

View file

@ -0,0 +1,23 @@
package moe.lava.banksia.core.data.sources.route
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.sqld.RouteQueries
import moe.lava.banksia.core.sqld.mappers.asDb
internal class RouteLocalDataSource(private val queries: RouteQueries) {
suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() }
suspend fun getAll() = withContext(Dispatchers.IO) { queries.getAll().executeAsList() }
// suspend fun getByTrip(tripId: String) = dao.getByTrip(tripId)
suspend fun save(vararg routes: Route) {
withContext(Dispatchers.IO) {
queries.transaction {
routes.forEach {
queries.insert(it.asDb())
}
}
}
}
}

View file

@ -0,0 +1,12 @@
package moe.lava.banksia.core.data.sources.route
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import moe.lava.banksia.core.model.Route
internal class RouteRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("routes/${id}").body<Route>()
suspend fun getByPattern(patternId: Long) = client.get("routes/by_pattern/${patternId}").body<Route>()
suspend fun getAll() = client.get("routes").body<List<Route>>()
}

View file

@ -0,0 +1,22 @@
package moe.lava.banksia.core.data.sources.stop
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import moe.lava.banksia.core.model.Stop
import moe.lava.banksia.core.sqld.StopQueries
import moe.lava.banksia.core.sqld.mappers.asDb
internal class StopLocalDataSource(private val queries: StopQueries) {
suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() }
suspend fun getByRoute(id: String) = withContext(Dispatchers.IO) { queries.getByRoute(id).executeAsList() }
suspend fun save(vararg stops: Stop) {
withContext(Dispatchers.IO) {
queries.transaction {
stops.forEach {
queries.insert(it.asDb())
}
}
}
}
}

View file

@ -1,11 +1,11 @@
package moe.lava.banksia.client.datasource.remote package moe.lava.banksia.core.data.sources.stop
import io.ktor.client.HttpClient import io.ktor.client.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.model.Stop import moe.lava.banksia.core.model.Stop
class StopRemoteDataSource(val client: HttpClient) { internal class StopRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("stops/${id}").body<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>>()
} }

View file

@ -0,0 +1,13 @@
package moe.lava.banksia.core.data
import moe.lava.banksia.core.sqld.sqldDiModule
import org.koin.core.module.Module
import org.koin.dsl.module
internal expect val platformModule: Module
val dataDiModule = module {
includes(platformModule)
includes(sqldDiModule)
includes(stopTimeDataDiModule)
}

View file

@ -0,0 +1,9 @@
package moe.lava.banksia.core.data.repositories
import moe.lava.banksia.core.model.Route
interface RouteRepository {
suspend fun get(id: String): Route?
suspend fun getByPattern(patternId: Long): Route?
suspend fun getAll(): List<Route>
}

View file

@ -0,0 +1,8 @@
package moe.lava.banksia.core.data.repositories
import moe.lava.banksia.core.model.Stop
interface StopRepository {
suspend fun get(id: String): Stop
suspend fun getByRoute(id: String): List<Stop>
}

View file

@ -0,0 +1,7 @@
package moe.lava.banksia.core.data
import org.koin.dsl.module
internal actual val platformModule = module {
}

View file

@ -0,0 +1,53 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.sqldelight)
}
kotlin {
android {
namespace = "moe.lava.banksia.core.sqld"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
androidMain.dependencies {
implementation(libs.sqldelight.driver.android)
}
commonMain.dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(projects.core)
}
nativeMain.dependencies {
implementation(libs.sqldelight.driver.native)
}
jvmMain.dependencies {
implementation(libs.sqldelight.driver.jvm)
}
}
}
sqldelight {
databases {
register("BanksiaDatabase") {
packageName.set("moe.lava.banksia.core.sqld")
schemaOutputDirectory.set(file("src/commonMain/sqldelight/schema"))
}
}
}

View file

@ -0,0 +1,14 @@
package moe.lava.banksia.core.sqld
import android.content.Context
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
actual class DatabaseManager : KoinComponent {
actual val database by lazy {
val ctx = get<Context>().applicationContext
val driver = AndroidSqliteDriver(BanksiaDatabase.Schema, ctx, "${DBNAME}.db")
BanksiaDatabase(driver)
}
}

View file

@ -0,0 +1,7 @@
package moe.lava.banksia.core.sqld
internal const val DBNAME = "timetable"
expect class DatabaseManager() {
val database: BanksiaDatabase
}

View file

@ -0,0 +1,17 @@
package moe.lava.banksia.core.sqld
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val sqldDiModule = module {
singleOf(::DatabaseManager)
factory { get<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 }
}

View file

@ -0,0 +1,14 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.model.RouteType
import moe.lava.banksia.core.sqld.Route as DbRoute
fun DbRoute.asModel() = Route(
id = id,
type = RouteType.from(type.toInt()),
number = number,
name = name,
)
fun Route.asDb() = DbRoute(id, type.value.toLong(), number, name)

View file

@ -0,0 +1,21 @@
package moe.lava.banksia.core.sqld.mappers
import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.model.Service
import moe.lava.banksia.core.util.deserialiseDaysBitflag
import moe.lava.banksia.core.util.serialise
import moe.lava.banksia.core.sqld.Service as DbService
fun DbService.asModel() = Service(
id = id,
days = days.toInt().deserialiseDaysBitflag(),
start = LocalDate.fromEpochDays(start),
end = LocalDate.fromEpochDays(end),
)
fun Service.asDb() = DbService(
id = id,
days = days.serialise().toLong(),
start = start.toEpochDays(),
end = end.toEpochDays(),
)

View file

@ -0,0 +1,17 @@
package moe.lava.banksia.core.sqld.mappers
import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.model.ServiceException
import moe.lava.banksia.core.sqld.ServiceException as DbServiceException
fun DbServiceException.asModel() = ServiceException(
serviceId = serviceId,
date = LocalDate.fromEpochDays(date),
type = type.toInt(),
)
fun ServiceException.asDb() = DbServiceException(
serviceId = serviceId,
type = date.toEpochDays(),
date = type.toLong(),
)

View file

@ -0,0 +1,52 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Shape
import moe.lava.banksia.core.model.ShapePath
import moe.lava.banksia.core.util.Point
import moe.lava.banksia.core.sqld.Shape as DbShape
fun DbShape.asModel() = Shape(
id = id,
path = bytesToPath(path),
)
fun Shape.asDb() = DbShape(
id = id,
path = bytesFromPath(path),
)
private fun bytesToPath(value: ByteArray): ShapePath {
return value
.asSequence()
.asIterable()
.chunked(8) {
(it[0].toLong() and 0xFF) or
(it[1].toLong() and 0xFF shl 8) or
(it[2].toLong() and 0xFF shl 16) or
(it[3].toLong() and 0xFF shl 24) or
(it[4].toLong() and 0xFF shl 32) or
(it[5].toLong() and 0xFF shl 40) or
(it[6].toLong() and 0xFF shl 48) or
(it[7].toLong() and 0xFF shl 56)
}
.map { Double.fromBits(it) }
.chunked(2)
.map { (lat, lng) -> Point(lat, lng) }
.toList()
}
private fun bytesFromPath(path: ShapePath): ByteArray {
return path
.flatMap { (lat, lng) -> listOf(lat.toBits(), lng.toBits()) }
.flatMap { i -> listOf(
i.toByte(),
(i shr 8).toByte(),
(i shr 16).toByte(),
(i shr 24).toByte(),
(i shr 32).toByte(),
(i shr 40).toByte(),
(i shr 48).toByte(),
(i shr 56).toByte(),
) }
.toByteArray()
}

View file

@ -0,0 +1,26 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Stop
import moe.lava.banksia.core.util.Point
import moe.lava.banksia.core.sqld.Stop as DbStop
fun DbStop.asModel() = Stop(
id = id,
name = name,
pos = Point(lat, lng),
parent = parent,
hasWheelChairBoarding = hasWheelChairBoarding == 1L,
level = level,
platformCode = platformCode,
)
fun Stop.asDb() = DbStop(
id = id,
name = name,
lat = pos.lat,
lng = pos.lng,
parent = parent,
hasWheelChairBoarding = if (hasWheelChairBoarding) 1L else 0L,
level = level,
platformCode = platformCode
)

View file

@ -0,0 +1,27 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.FutureTime
import moe.lava.banksia.core.model.FutureTime.Companion.asInt
import moe.lava.banksia.core.model.StopTime
import moe.lava.banksia.core.model.TimeType
import moe.lava.banksia.core.sqld.StopTime as DbStopTime
fun DbStopTime.asModel() = StopTime(
patternId = patternId,
stopId = stopId,
time = TimeType.Undated(
arrival = FutureTime.fromInt((departureTime + arrivalDelta).toInt()),
departure = FutureTime.fromInt(departureTime.toInt()),
),
pickupType = pickupType.toInt(),
dropOffType = dropOffType.toInt(),
)
fun StopTime.Undated.asDb() = DbStopTime(
patternId = patternId,
stopId = stopId,
arrivalDelta = (time.arrival.asInt() - time.departure.asInt()).toLong(),
departureTime = time.departure.asInt().toLong(),
pickupType = pickupType.toLong(),
dropOffType = dropOffType.toLong(),
)

View file

@ -0,0 +1,23 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.StopTime
import moe.lava.banksia.core.model.StoppingPattern
import moe.lava.banksia.core.model.TimeType
import moe.lava.banksia.core.sqld.StoppingPattern as DbStoppingPattern
fun <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,
)

View file

@ -0,0 +1,27 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Service
import moe.lava.banksia.core.model.StoppingPattern
import moe.lava.banksia.core.model.Trip
import moe.lava.banksia.core.sqld.Trip as DbTrip
fun DbTrip.asModel(pattern: StoppingPattern.Undated, service: Service): Trip.Undated {
if (serviceId != service.id) {
throw IllegalArgumentException("trip and service id mismatch (${serviceId} != ${service.id})")
}
return Trip(
id = gtfsId,
pattern = pattern,
service = service,
directionId = directionId.toInt(),
blockId = blockId.toString(),
)
}
fun Trip.Undated.asDb() = DbTrip(
gtfsId = id,
patternId = pattern.id,
serviceId = service.id,
directionId = directionId.toLong(),
blockId = blockId?.toLong(),
)

View file

@ -0,0 +1,20 @@
CREATE TABLE Route (
id TEXT PRIMARY KEY NOT NULL,
type INTEGER NOT NULL,
number TEXT,
name TEXT NOT NULL
);
getAll:
SELECT * FROM Route;
get:
SELECT * FROM Route WHERE id == ?;
getByPattern:
SELECT Route.* FROM Route
INNER JOIN StoppingPattern ON Route.id == StoppingPattern.routeId
WHERE StoppingPattern.id == :patternId;
insert:
INSERT OR REPLACE INTO Route VALUES ?;

View file

@ -0,0 +1,11 @@
CREATE TABLE Service (
id TEXT PRIMARY KEY NOT NULL,
days INTEGER NOT NULL,
start INTEGER NOT NULL,
end INTEGER NOT NULL
);
CREATE INDEX idx_Service_days ON Service (days);
insert:
INSERT INTO Service VALUES ?;

View file

@ -0,0 +1,9 @@
CREATE TABLE ServiceException (
serviceId TEXT NOT NULL,
type INTEGER NOT NULL,
date INTEGER NOT NULL,
PRIMARY KEY (serviceId, type)
);
insert:
INSERT INTO ServiceException VALUES ?;

View file

@ -0,0 +1,7 @@
CREATE TABLE Shape (
id TEXT PRIMARY KEY NOT NULL,
path BLOB NOT NULL
);
insert:
INSERT INTO Shape VALUES ?;

View file

@ -0,0 +1,54 @@
CREATE TABLE Stop (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
lat REAL NOT NULL,
lng REAL NOT NULL,
parent TEXT REFERENCES Stop(id),
hasWheelChairBoarding INTEGER NOT NULL,
level TEXT,
platformCode TEXT
);
CREATE INDEX idx_Stop_parent ON Stop (parent);
getAll:
SELECT * FROM Stop;
getAllParentless:
SELECT * FROM Stop WHERE platformCode IS NOT NULL AND parent IS NULL;
get:
SELECT * FROM Stop WHERE id == ?;
getMany:
SELECT * FROM Stop WHERE id IN ?;
insert:
INSERT INTO Stop VALUES ?;
updateParents:
UPDATE Stop SET parent = ? WHERE id IN ?;
getByRoute:
SELECT Stop.* FROM Stop
INNER JOIN StopTime ON StopTime.stopId == Stop.id
INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId
WHERE StoppingPattern.routeId == :id
GROUP BY Stop.id;
-- I vibecoded this, sorry
getParentsByRoute:
WITH RECURSIVE Tree AS (
SELECT Stop.* FROM Stop
INNER JOIN StopTime ON StopTime.stopId == Stop.id
INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId
WHERE StoppingPattern.routeId == :id
GROUP BY Stop.id
UNION ALL
SELECT s.*
FROM Stop s
INNER JOIN Tree t ON s.id = t.parent
)
SELECT DISTINCT * FROM Tree WHERE parent IS NULL;

View file

@ -0,0 +1,45 @@
CREATE TABLE StopTime (
patternId INTEGER NOT NULL REFERENCES StoppingPattern (id),
stopId TEXT NOT NULL REFERENCES Stop (id),
arrivalDelta INTEGER NOT NULL,
departureTime INTEGER NOT NULL,
pickupType INTEGER NOT NULL,
dropOffType INTEGER NOT NULL,
PRIMARY KEY (patternId, stopId)
) WITHOUT ROWID;
CREATE INDEX idx_StopTime_stopId ON StopTime (stopId);
insert:
INSERT OR REPLACE INTO StopTime VALUES ?;
getForStopDated:
SELECT DISTINCT StopTime.* FROM StopTime
INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
INNER JOIN Trip ON Trip.serviceId == Service.id
INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
WHERE StopTime.patternId == StoppingPattern.id
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL;
getExtendedForStop:
SELECT DISTINCT
StopTime.patternId,
StopTime.arrivalDelta,
StopTime.departureTime,
StoppingPattern.headsign,
Route.type AS routeType,
Route.number AS routeNumber,
Route.name AS routeName,
Stop.platformCode AS stopPlatformCode
FROM StopTime
INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
INNER JOIN Trip ON Trip.serviceId == Service.id
INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
INNER JOIN Route ON Route.id == StoppingPattern.routeId
INNER JOIN Stop ON Stop.id == StopTime.stopId
WHERE StopTime.patternId == StoppingPattern.id
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL;

View file

@ -0,0 +1,13 @@
CREATE TABLE StoppingPattern (
id INTEGER PRIMARY KEY NOT NULL,
routeId TEXT NOT NULL REFERENCES Route (id),
shapeId TEXT NOT NULL REFERENCES Shape (id),
headsign TEXT NOT NULL,
wheelchairAccessible INTEGER NOT NULL
);
insert:
INSERT OR REPLACE INTO StoppingPattern VALUES ?;
get:
SELECT * FROM StoppingPattern WHERE id == :id;

View file

@ -0,0 +1,13 @@
CREATE TABLE Trip (
gtfsId TEXT PRIMARY KEY NOT NULL,
patternId INTEGER NOT NULL REFERENCES StoppingPattern (id),
serviceId TEXT NOT NULL REFERENCES Service (id),
blockId INTEGER,
directionId INTEGER NOT NULL
);
CREATE INDEX idx_Trip_patternId ON Trip (patternId);
CREATE INDEX idx_Trip_serviceId ON Trip (serviceId);
insert:
INSERT OR REPLACE INTO Trip VALUES ?;

Binary file not shown.

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.core.sqld
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import org.koin.core.component.KoinComponent
actual class DatabaseManager : KoinComponent {
actual val database by lazy {
val driver = NativeSqliteDriver(BanksiaDatabase.Schema, "${DBNAME}.db")
BanksiaDatabase(driver)
}
}

View file

@ -0,0 +1,56 @@
package moe.lava.banksia.core.sqld
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import moe.lava.banksia.core.util.error
import org.koin.core.component.KoinComponent
import java.io.File
import java.util.Properties
import kotlin.system.exitProcess
actual class DatabaseManager : KoinComponent {
private var driver = connect()
actual val database get() = BanksiaDatabase(driver)
private fun connect(path: String = "./data/${DBNAME}.db") =
JdbcSqliteDriver("jdbc:sqlite:${path}", Properties(), BanksiaDatabase.Schema)
.apply { execute(null, "PRAGMA journal_mode = OFF;", 0) }
fun makeAlt() = run {
File("./data/${DBNAME}_alt.db").takeIf { it.exists() }?.delete()
val driver = connect("./data/${DBNAME}_alt.db")
BanksiaDatabase(driver) to { driver.close() }
}
fun swap(scope: CoroutineScope = CoroutineScope(Dispatchers.IO)) {
val live = File("./data/${DBNAME}.db")
val alt = File("./data/${DBNAME}_alt.db")
val old = File("./data/${DBNAME}_old.db")
if (live.takeIf { it.exists() }?.renameTo(old) == false) {
error("DatabaseManager", "Failed to rename database from live to old (${live.absolutePath} -> ${old.absolutePath})")
return
}
if (alt.takeIf { it.exists() }?.renameTo(live) == false) {
error("DatabaseManager", "Failed to rename database from alt to live, trying to undo.. (${alt.absolutePath} -> ${live.absolutePath})")
if (!live.renameTo(old)) {
error("DatabaseManager", "Failed to undo, critical failure, exiting..")
exitProcess(1)
}
return
}
val oldDriver = driver
driver = connect()
scope.launch {
delay(5000)
if (old.takeIf { it.exists() }?.delete() == false) {
error("DatabaseManager", "Failed to unlink old database, stray files! (${old.absolutePath})")
}
oldDriver.close()
}
}
}

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.util package moe.lava.banksia.core.util
import android.util.Log import android.util.Log

View file

@ -6,6 +6,7 @@ object Constants {
const val opendataKey: String = "" const val opendataKey: String = ""
const val serverUrl: String = "https://banksia.lava.moe/api/" const val serverUrl: String = "https://banksia.lava.moe/api/"
// TODO // TODO
const val devMode: Boolean = false var devMode: Boolean = false
const val updateKey: String = "" const val updateKey: String = ""
const val protomapsKey: String = ""
} }

View file

@ -0,0 +1,3 @@
package moe.lava.banksia.core.endpoints
object Endpoint

View file

@ -1,6 +1,10 @@
package moe.lava.banksia.model package moe.lava.banksia.core.model
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime import kotlinx.datetime.LocalTime
import kotlinx.datetime.atTime
import kotlinx.datetime.plus
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveKind
@ -8,7 +12,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import moe.lava.banksia.model.FutureTime.Companion.asInt import moe.lava.banksia.core.model.FutureTime.Companion.asInt
@Serializable(FutureTimeSerialiser::class) @Serializable(FutureTimeSerialiser::class)
data class FutureTime( data class FutureTime(
@ -39,6 +43,10 @@ data class FutureTime(
val minute = time.minute val minute = time.minute
val second = time.second val second = time.second
val trueHour = time.hour + (if (dayOffset) 24 else 0) val trueHour = time.hour + (if (dayOffset) 24 else 0)
fun atDate(date: LocalDate) = date
.let { if (dayOffset) date.plus(1, DateTimeUnit.DAY) else date }
.atTime(time)
} }
object FutureTimeSerialiser: KSerializer<FutureTime> { object FutureTimeSerialiser: KSerializer<FutureTime> {

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.model package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.model package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -13,4 +13,8 @@ enum class RouteType(val value: Int) {
SkyBus(11), SkyBus(11),
Interstate(10), Interstate(10),
; ;
companion object {
fun from(value: Int) = entries.first { it.value == value }
}
} }

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.model package moe.lava.banksia.core.model
data class Run( data class Run(
val ref: String, val ref: String,

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.model package moe.lava.banksia.core.model
import kotlinx.datetime.DayOfWeek import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.core.model
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@Serializable
data class ServiceException(
val serviceId: String,
val date: LocalDate,
val type: Int,
)

View file

@ -1,7 +1,7 @@
package moe.lava.banksia.model package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import moe.lava.banksia.util.Point import moe.lava.banksia.core.util.Point
typealias ShapePath = List<Point> typealias ShapePath = List<Point>

View file

@ -1,15 +1,15 @@
package moe.lava.banksia.model package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import moe.lava.banksia.util.Point import moe.lava.banksia.core.util.Point
@Serializable @Serializable
data class Stop( data class Stop(
val id: String, val id: String,
val name: String, val name: String,
val pos: Point, val pos: Point,
val parent: String, val parent: String?,
val hasWheelChairBoarding: Boolean, val hasWheelChairBoarding: Boolean,
val level: String, val level: String?,
val platformCode: String, val platformCode: String?,
) )

View file

@ -0,0 +1,45 @@
package moe.lava.banksia.core.model
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
data class StopTime<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,
)

View file

@ -0,0 +1,16 @@
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>
}

View file

@ -0,0 +1,15 @@
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>
}

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.model package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -1,5 +1,6 @@
package moe.lava.banksia.util package moe.lava.banksia.core.util
/** Wraps an arbitrary value, such that equality checks are forced to be done by reference */
class BoxedValue<T>(val value: T) { class BoxedValue<T>(val value: T) {
operator fun component1() = value operator fun component1() = value

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.util package moe.lava.banksia.core.util
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View file

@ -0,0 +1,36 @@
package moe.lava.banksia.core.util
import kotlinx.datetime.DayOfWeek
private fun Int.check(other: Int) = (this and other) != 0
fun Int.deserialiseDaysBitflag(): List<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
}
}

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.util package moe.lava.banksia.core.util
fun error(tag: String, throwable: Throwable) = error(tag, "", throwable) fun error(tag: String, throwable: Throwable) = error(tag, "", throwable)
expect fun log(tag: String, msg: String) expect fun log(tag: String, msg: String)

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.util package moe.lava.banksia.core.util
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.util package moe.lava.banksia.core.util
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -16,7 +16,12 @@ import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import moe.lava.banksia.Constants import moe.lava.banksia.core.Constants
import moe.lava.banksia.core.model.RouteType
import moe.lava.banksia.core.util.LoopFlow.Companion.initWith
import moe.lava.banksia.core.util.error
import moe.lava.banksia.core.util.log
import moe.lava.banksia.core.util.loopFlow
import moe.lava.banksia.data.ptv.structures.PtvDeparture import moe.lava.banksia.data.ptv.structures.PtvDeparture
import moe.lava.banksia.data.ptv.structures.PtvDirection import moe.lava.banksia.data.ptv.structures.PtvDirection
import moe.lava.banksia.data.ptv.structures.PtvRoute import moe.lava.banksia.data.ptv.structures.PtvRoute
@ -24,11 +29,6 @@ import moe.lava.banksia.data.ptv.structures.PtvRouteType
import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType
import moe.lava.banksia.data.ptv.structures.PtvRun import moe.lava.banksia.data.ptv.structures.PtvRun
import moe.lava.banksia.data.ptv.structures.PtvStop import moe.lava.banksia.data.ptv.structures.PtvStop
import moe.lava.banksia.model.RouteType
import moe.lava.banksia.util.LoopFlow.Companion.initWith
import moe.lava.banksia.util.error
import moe.lava.banksia.util.log
import moe.lava.banksia.util.loopFlow
import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.encodeUtf8
import kotlin.random.Random import kotlin.random.Random

View file

@ -2,7 +2,7 @@ package moe.lava.banksia.data.ptv.structures
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import moe.lava.banksia.model.RouteType import moe.lava.banksia.core.model.RouteType
@Serializable @Serializable
data class PtvRoute( data class PtvRoute(

View file

@ -7,9 +7,9 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import moe.lava.banksia.model.RouteType import moe.lava.banksia.core.model.RouteType
private object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> { object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
PtvRouteType::class.qualifiedName!!, PtvRouteType::class.qualifiedName!!,
PrimitiveKind.INT) PrimitiveKind.INT)

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.util package moe.lava.banksia.core.util
actual fun log(tag: String, msg: String) { actual fun log(tag: String, msg: String) {
TODO("Not yet implemented") TODO("Not yet implemented")

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.util package moe.lava.banksia.core.util
actual fun log(tag: String, msg: String) { actual fun log(tag: String, msg: String) {
println("[$tag] $msg") println("[$tag] $msg")

View file

@ -0,0 +1,64 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.ksp)
}
kotlin {
android {
namespace = "moe.lava.banksia.core.stoptime"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
freeCompilerArgs.add("-Xexpect-actual-classes")
}
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
val clientMain by creating {
dependsOn(commonMain.get())
}
androidMain.get().dependsOn(clientMain)
iosArm64Main.get().dependsOn(clientMain)
iosSimulatorArm64Main.get().dependsOn(clientMain)
androidMain.dependencies {
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
implementation(projects.core)
implementation(projects.core.sqld)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
jvmMain.dependencies {
implementation(libs.koin.ktor)
implementation(libs.ktor.server.core)
}
}
}

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.core.data
import moe.lava.banksia.core.data.repositories.StopTimeRepository
import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
internal actual val platformModule = module {
singleOf(::StopTimeRepository)
singleOf(::StopTimeRemoteDataSource)
}

Some files were not shown because too many files have changed in this diff Show more