feat: server-handled routes and stops
This commit is contained in:
parent
efba64ea90
commit
58ee095522
61 changed files with 1634 additions and 349 deletions
|
|
@ -41,6 +41,7 @@ kotlin {
|
|||
implementation(libs.play.services.location)
|
||||
implementation(libs.play.services.maps)
|
||||
implementation(libs.maps.compose)
|
||||
implementation(libs.maps.compose.utils)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(compose.runtime)
|
||||
|
|
@ -53,8 +54,13 @@ kotlin {
|
|||
implementation(libs.androidx.lifecycle.viewmodel)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.contentnegotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.moko.geo)
|
||||
implementation(libs.moko.geo.compose)
|
||||
implementation(projects.shared)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ import com.google.maps.android.compose.rememberCameraPositionState
|
|||
import com.google.maps.android.compose.rememberUpdatedMarkerState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import moe.lava.banksia.R
|
||||
import moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.components.RouteIcon
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import moe.lava.banksia.util.Point
|
||||
|
|
@ -67,7 +67,7 @@ actual fun getScreenHeight(): Int {
|
|||
actual fun Maps(
|
||||
modifier: Modifier,
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
@ -135,7 +135,7 @@ actual fun Maps(
|
|||
zIndex = 0f,
|
||||
state = state,
|
||||
onClick = {
|
||||
onEvent(BanksiaEvent.SelectStop(marker.type to marker.id))
|
||||
onEvent(MapScreenEvent.SelectStop(marker.type to marker.id))
|
||||
false
|
||||
}
|
||||
) {
|
||||
|
|
@ -155,7 +155,7 @@ actual fun Maps(
|
|||
zIndex = 1f,
|
||||
state = state,
|
||||
onClick = {
|
||||
onEvent(BanksiaEvent.SelectRun(marker.ref))
|
||||
onEvent(MapScreenEvent.SelectRun(marker.ref))
|
||||
false
|
||||
}
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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>>()
|
||||
}
|
||||
|
|
@ -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.Stop
|
||||
|
||||
class StopRemoteDataSource(val client: HttpClient) {
|
||||
suspend fun get(id: String) = client.get("/stops/${id}").body<Stop>()
|
||||
suspend fun getByRoute(id: String) = client.get("/route_stops/${id}").body<List<Stop>>()
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package moe.lava.banksia.client.di
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.defaultRequest
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import moe.lava.banksia.Constants
|
||||
import moe.lava.banksia.client.datasource.local.RouteLocalDataSource
|
||||
import moe.lava.banksia.client.datasource.local.StopLocalDataSource
|
||||
import moe.lava.banksia.client.datasource.remote.RouteRemoteDataSource
|
||||
import moe.lava.banksia.client.datasource.remote.StopRemoteDataSource
|
||||
import moe.lava.banksia.client.repository.RouteRepository
|
||||
import moe.lava.banksia.client.repository.StopRepository
|
||||
import moe.lava.banksia.data.ptv.PtvService
|
||||
import moe.lava.banksia.ui.screens.MapScreenViewModel
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val ClientModule = module {
|
||||
// HTTP Clients
|
||||
singleOf(::PtvService)
|
||||
single {
|
||||
HttpClient() {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
defaultRequest {
|
||||
url(Constants.serverUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data sources
|
||||
singleOf(::RouteLocalDataSource)
|
||||
singleOf(::RouteRemoteDataSource)
|
||||
singleOf(::StopLocalDataSource)
|
||||
singleOf(::StopRemoteDataSource)
|
||||
|
||||
// Repositories
|
||||
singleOf(::RouteRepository)
|
||||
singleOf(::StopRepository)
|
||||
|
||||
// ViewModel
|
||||
viewModelOf(::MapScreenViewModel)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package moe.lava.banksia.client.repository
|
||||
|
||||
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,
|
||||
) {
|
||||
suspend fun getAll() =
|
||||
local
|
||||
.getAll()
|
||||
.map { it.asModel() }
|
||||
.ifEmpty {
|
||||
remote
|
||||
.getAll()
|
||||
.also { local.save(*it.toTypedArray()) }
|
||||
}
|
||||
|
||||
suspend fun get(id: String) = local.get(id)?.asModel() ?: remote.get(id)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package moe.lava.banksia.client.repository
|
||||
|
||||
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,
|
||||
) {
|
||||
suspend fun get(id: String) = local.get(id)?.asModel() ?: remote.get(id)
|
||||
suspend fun getByRoute(id: String) =
|
||||
local
|
||||
.getByRoute(id)
|
||||
.map { it.asModel() }
|
||||
.ifEmpty { null }
|
||||
?: remote.getByRoute(id)
|
||||
}
|
||||
|
|
@ -1,194 +1,21 @@
|
|||
package moe.lava.banksia.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.add
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeContent
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.BottomSheetScaffold
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import dev.icerock.moko.geo.compose.BindLocationTrackerEffect
|
||||
import dev.icerock.moko.geo.compose.LocationTrackerAccuracy
|
||||
import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.my_location_24
|
||||
import moe.lava.banksia.ui.layout.InfoPanel
|
||||
import moe.lava.banksia.ui.layout.Searcher
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import moe.lava.banksia.ui.platform.maps.Maps
|
||||
import moe.lava.banksia.ui.platform.maps.getScreenHeight
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
import moe.lava.banksia.util.Point
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.roundToInt
|
||||
import moe.lava.banksia.client.di.ClientModule
|
||||
import moe.lava.banksia.di.CommonModules
|
||||
import moe.lava.banksia.ui.screens.MapScreen
|
||||
import org.koin.compose.KoinMultiplatformApplication
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
import org.koin.dsl.koinConfiguration
|
||||
|
||||
val MELBOURNE = Point(-37.8136, 144.9631)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, KoinExperimentalAPI::class)
|
||||
@Composable
|
||||
fun App(
|
||||
viewModel: BanksiaViewModel = viewModel()
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best)
|
||||
val locationTracker = remember { locationFactory.createLocationTracker() }
|
||||
BindLocationTrackerEffect(locationTracker)
|
||||
viewModel.bindTracker(locationTracker)
|
||||
scope.launch { locationTracker.startTracking() }
|
||||
|
||||
val infoState by viewModel.infoState.collectAsStateWithLifecycle()
|
||||
val mapState by viewModel.mapState.collectAsStateWithLifecycle()
|
||||
val searchState by viewModel.searchState.collectAsStateWithLifecycle()
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(
|
||||
initialValue = SheetValue.Hidden,
|
||||
skipHiddenState = false
|
||||
)
|
||||
)
|
||||
|
||||
val sheetState = scaffoldState.bottomSheetState
|
||||
val extInsets = if (
|
||||
sheetState.currentValue != SheetValue.Hidden ||
|
||||
sheetState.targetValue != SheetValue.Hidden
|
||||
) {
|
||||
val offset = runCatching { sheetState.requireOffset() }
|
||||
val scaffoldOffset = offset.getOrDefault(0.0f).roundToInt()
|
||||
(getScreenHeight() - scaffoldOffset - WindowInsets.Companion.safeDrawing.getBottom(
|
||||
LocalDensity.current)).coerceAtLeast(0)
|
||||
} else 0
|
||||
|
||||
LaunchedEffect(infoState) {
|
||||
if (infoState !is InfoPanelState.None)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
}
|
||||
|
||||
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
|
||||
var sheetSwipeEnabled by rememberSaveable { mutableStateOf(true) }
|
||||
var handleHeight by remember { mutableStateOf(0.dp) }
|
||||
var peekHeight by remember { mutableStateOf(0.dp) }
|
||||
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
||||
|
||||
BanksiaTheme {
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
sheetContent = {
|
||||
InfoPanel(
|
||||
state = infoState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
onPeekHeightChange = { peekHeight = it },
|
||||
)
|
||||
},
|
||||
sheetDragHandle = {
|
||||
val density = LocalDensity.current
|
||||
Box(
|
||||
Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
.onSizeChanged {
|
||||
handleHeight = with(density) { it.height.toDp() }
|
||||
}
|
||||
) {
|
||||
BottomSheetDefaults.DragHandle(modifier = Modifier.Companion.align(Alignment.Companion.Center))
|
||||
}
|
||||
},
|
||||
sheetSwipeEnabled = sheetSwipeEnabled,
|
||||
) {
|
||||
Maps(
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
state = mapState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
cameraPositionFlow = viewModel.cameraChangeEmitter,
|
||||
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
||||
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||
}, bottom = extInsets),
|
||||
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||
)
|
||||
Searcher(
|
||||
state = searchState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
expanded = searchExpandedState,
|
||||
onExpandedChange = {
|
||||
searchExpandedState = it
|
||||
if (it)
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
},
|
||||
)
|
||||
|
||||
PredictiveBackHandler(scaffoldState.bottomSheetState.currentValue != SheetValue.Hidden) { progress ->
|
||||
sheetSwipeEnabled = false
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded) {
|
||||
peekHeightMultiplier = 1F - backEvent.progress
|
||||
}
|
||||
}
|
||||
if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded)
|
||||
scope.launch {
|
||||
scaffoldState.bottomSheetState.hide()
|
||||
peekHeightMultiplier = 1F
|
||||
viewModel.handleEvent(BanksiaEvent.DismissState)
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
peekHeightMultiplier = 1F
|
||||
}
|
||||
sheetSwipeEnabled = true
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.Companion.windowInsetsPadding(
|
||||
WindowInsets.Companion.safeContent.add(
|
||||
WindowInsets(bottom = extInsets)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Companion.BottomEnd
|
||||
) {
|
||||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
onClick = { viewModel.centreCameraToLocation() },
|
||||
) {
|
||||
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
|
||||
}
|
||||
}
|
||||
}
|
||||
fun App() {
|
||||
KoinMultiplatformApplication(config = koinConfiguration {
|
||||
modules(CommonModules, ClientModule)
|
||||
}) {
|
||||
MapScreen()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.model.RouteType.Interstate
|
||||
import moe.lava.banksia.model.RouteType.MetroBus
|
||||
import moe.lava.banksia.model.RouteType.MetroTrain
|
||||
import moe.lava.banksia.model.RouteType.MetroTram
|
||||
import moe.lava.banksia.model.RouteType.RegionalBus
|
||||
import moe.lava.banksia.model.RouteType.RegionalCoach
|
||||
import moe.lava.banksia.model.RouteType.RegionalTrain
|
||||
import moe.lava.banksia.model.RouteType.SkyBus
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.bus
|
||||
import moe.lava.banksia.resources.bus_background
|
||||
|
|
@ -33,12 +42,51 @@ data class RouteTypeProperties(
|
|||
val icon: DrawableResource,
|
||||
)
|
||||
|
||||
const val TRAIN_BLUE = 0xFF0072CE
|
||||
const val TRAM_GREEN = 0xFF78BE20
|
||||
const val BUS_ORANGE = 0xFFFF8200
|
||||
const val VLINE_PURPLE = 0xFF8F1A95
|
||||
|
||||
fun RouteType.getUIProperties(): RouteTypeProperties {
|
||||
val colour = when (this) {
|
||||
MetroTrain -> TRAIN_BLUE
|
||||
MetroTram -> TRAM_GREEN
|
||||
MetroBus -> BUS_ORANGE
|
||||
RegionalTrain -> VLINE_PURPLE
|
||||
RegionalCoach -> VLINE_PURPLE
|
||||
RegionalBus -> VLINE_PURPLE
|
||||
SkyBus -> BUS_ORANGE
|
||||
Interstate -> BUS_ORANGE
|
||||
}
|
||||
|
||||
val (drawable, background, icon) = when (this) {
|
||||
MetroTrain,
|
||||
RegionalTrain,
|
||||
Interstate -> Triple(
|
||||
Res.drawable.train, Res.drawable.train_background, Res.drawable.train_icon
|
||||
)
|
||||
|
||||
MetroTram -> Triple(
|
||||
Res.drawable.tram, Res.drawable.tram_background, Res.drawable.tram_icon
|
||||
)
|
||||
|
||||
MetroBus,
|
||||
RegionalCoach,
|
||||
RegionalBus,
|
||||
SkyBus -> Triple(
|
||||
Res.drawable.bus, Res.drawable.bus_background, Res.drawable.bus_icon
|
||||
)
|
||||
}
|
||||
|
||||
return RouteTypeProperties(Color(colour), drawable, background, icon)
|
||||
}
|
||||
|
||||
fun PtvRouteType.getUIProperties(): RouteTypeProperties {
|
||||
val colour = when (this) {
|
||||
PtvRouteType.TRAIN -> Color(0xFF0072CE)
|
||||
PtvRouteType.TRAM -> Color(0xFF78BE20)
|
||||
PtvRouteType.BUS, PtvRouteType.NIGHT_BUS -> Color(0xFFFF8200)
|
||||
PtvRouteType.VLINE -> Color(0xFF8F1A95)
|
||||
PtvRouteType.TRAIN -> Color(TRAIN_BLUE)
|
||||
PtvRouteType.TRAM -> Color(TRAM_GREEN)
|
||||
PtvRouteType.BUS, PtvRouteType.NIGHT_BUS -> Color(BUS_ORANGE)
|
||||
PtvRouteType.VLINE -> Color(VLINE_PURPLE)
|
||||
}
|
||||
val (drawable, background, icon) = when (this) {
|
||||
PtvRouteType.TRAM -> Triple(
|
||||
|
|
@ -58,7 +106,7 @@ fun PtvRouteType.getUIProperties(): RouteTypeProperties {
|
|||
fun RouteIcon(
|
||||
modifier: Modifier = Modifier.Companion,
|
||||
size: Dp = 40.dp,
|
||||
routeType: PtvRouteType,
|
||||
routeType: RouteType,
|
||||
) {
|
||||
val properties = routeType.getUIProperties()
|
||||
Image(
|
||||
|
|
@ -80,9 +128,9 @@ const val ICON_PADDING = 0.25f
|
|||
@Composable
|
||||
private fun RouteIconPreview() {
|
||||
Row {
|
||||
RouteIcon(routeType = PtvRouteType.TRAIN)
|
||||
RouteIcon(routeType = PtvRouteType.TRAM)
|
||||
RouteIcon(routeType = PtvRouteType.BUS)
|
||||
RouteIcon(routeType = RouteType.MetroTrain)
|
||||
RouteIcon(routeType = RouteType.MetroTram)
|
||||
RouteIcon(routeType = RouteType.MetroBus)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ 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 moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.components.RouteIcon
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
|
||||
@Composable
|
||||
fun InfoPanel(
|
||||
state: InfoPanelState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
onPeekHeightChange: (Dp) -> Unit,
|
||||
) {
|
||||
if (state is InfoPanelState.None)
|
||||
|
|
@ -74,7 +74,7 @@ fun InfoPanel(
|
|||
@Composable
|
||||
private inline fun RouteInfoPanel(
|
||||
state: InfoPanelState.Route,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.Companion.fillMaxWidth()) {
|
||||
Row {
|
||||
|
|
@ -92,7 +92,7 @@ private inline fun RouteInfoPanel(
|
|||
@Composable
|
||||
private inline fun RunInfoPanel(
|
||||
state: InfoPanelState.Run,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.Companion.fillMaxWidth()) {
|
||||
Row {
|
||||
|
|
@ -110,7 +110,7 @@ private inline fun RunInfoPanel(
|
|||
@Composable
|
||||
private inline fun StopInfoPanel(
|
||||
state: InfoPanelState.Stop,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.Companion.fillMaxWidth()) {
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.components.RouteIcon
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.SearchState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Searcher(
|
||||
state: SearchState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
|
|
@ -55,7 +55,7 @@ fun Searcher(
|
|||
SearchBarDefaults.InputField(
|
||||
modifier = Modifier.Companion.padding(horizontal = 20.dp - animatedPadding),
|
||||
query = state.text,
|
||||
onQueryChange = { onEvent(BanksiaEvent.SearchUpdate(it)) },
|
||||
onQueryChange = { onEvent(MapScreenEvent.SearchUpdate(it)) },
|
||||
onSearch = {},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
|
|
@ -67,7 +67,7 @@ fun Searcher(
|
|||
contentDescription = null,
|
||||
modifier = Modifier.Companion.clickable {
|
||||
onEvent(
|
||||
BanksiaEvent.SearchUpdate(
|
||||
MapScreenEvent.SearchUpdate(
|
||||
""
|
||||
)
|
||||
)
|
||||
|
|
@ -92,8 +92,8 @@ fun Searcher(
|
|||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.clickable {
|
||||
onExpandedChange(false)
|
||||
onEvent(BanksiaEvent.SearchUpdate(""))
|
||||
onEvent(BanksiaEvent.SelectRoute(entry.routeId))
|
||||
onEvent(MapScreenEvent.SearchUpdate(""))
|
||||
onEvent(MapScreenEvent.SelectRoute(entry.routeId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import moe.lava.banksia.util.Point
|
||||
|
|
@ -18,7 +18,7 @@ expect fun getScreenHeight(): Int
|
|||
expect fun Maps(
|
||||
modifier: Modifier = Modifier.Companion,
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package moe.lava.banksia.ui.platform.maps
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
sealed class Marker {
|
||||
|
|
@ -9,14 +9,14 @@ sealed class Marker {
|
|||
|
||||
data class Stop(
|
||||
override val point: Point,
|
||||
val id: Int,
|
||||
val type: PtvRouteType,
|
||||
val id: String,
|
||||
val type: RouteType,
|
||||
val colour: Color,
|
||||
) : Marker()
|
||||
|
||||
data class Vehicle(
|
||||
override val point: Point,
|
||||
val ref: String,
|
||||
val type: PtvRouteType,
|
||||
val type: RouteType,
|
||||
) : Marker()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
package moe.lava.banksia.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.add
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeContent
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.BottomSheetScaffold
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.icerock.moko.geo.compose.BindLocationTrackerEffect
|
||||
import dev.icerock.moko.geo.compose.LocationTrackerAccuracy
|
||||
import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.my_location_24
|
||||
import moe.lava.banksia.ui.layout.InfoPanel
|
||||
import moe.lava.banksia.ui.layout.Searcher
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import moe.lava.banksia.ui.platform.maps.Maps
|
||||
import moe.lava.banksia.ui.platform.maps.getScreenHeight
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
import moe.lava.banksia.util.Point
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
val MELBOURNE = Point(-37.8136, 144.9631)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun MapScreen(
|
||||
viewModel: MapScreenViewModel = koinViewModel()
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best)
|
||||
val locationTracker = remember { locationFactory.createLocationTracker() }
|
||||
BindLocationTrackerEffect(locationTracker)
|
||||
viewModel.bindTracker(locationTracker)
|
||||
scope.launch { locationTracker.startTracking() }
|
||||
|
||||
val infoState by viewModel.infoState.collectAsStateWithLifecycle()
|
||||
val mapState by viewModel.mapState.collectAsStateWithLifecycle()
|
||||
val searchState by viewModel.searchState.collectAsStateWithLifecycle()
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(
|
||||
initialValue = SheetValue.Hidden,
|
||||
skipHiddenState = false
|
||||
)
|
||||
)
|
||||
|
||||
val sheetState = scaffoldState.bottomSheetState
|
||||
val extInsets = if (
|
||||
sheetState.currentValue != SheetValue.Hidden ||
|
||||
sheetState.targetValue != SheetValue.Hidden
|
||||
) {
|
||||
val offset = runCatching { sheetState.requireOffset() }
|
||||
val scaffoldOffset = offset.getOrDefault(0.0f).roundToInt()
|
||||
(getScreenHeight() - scaffoldOffset - WindowInsets.Companion.safeDrawing.getBottom(
|
||||
LocalDensity.current)).coerceAtLeast(0)
|
||||
} else 0
|
||||
|
||||
LaunchedEffect(infoState) {
|
||||
if (infoState !is InfoPanelState.None)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
}
|
||||
|
||||
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
|
||||
var sheetSwipeEnabled by rememberSaveable { mutableStateOf(true) }
|
||||
var handleHeight by remember { mutableStateOf(0.dp) }
|
||||
var peekHeight by remember { mutableStateOf(0.dp) }
|
||||
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
||||
|
||||
BanksiaTheme {
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
sheetContent = {
|
||||
InfoPanel(
|
||||
state = infoState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
onPeekHeightChange = { peekHeight = it },
|
||||
)
|
||||
},
|
||||
sheetDragHandle = {
|
||||
val density = LocalDensity.current
|
||||
Box(
|
||||
Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
.onSizeChanged {
|
||||
handleHeight = with(density) { it.height.toDp() }
|
||||
}
|
||||
) {
|
||||
BottomSheetDefaults.DragHandle(modifier = Modifier.Companion.align(Alignment.Companion.Center))
|
||||
}
|
||||
},
|
||||
sheetSwipeEnabled = sheetSwipeEnabled,
|
||||
) {
|
||||
Maps(
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
state = mapState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
cameraPositionFlow = viewModel.cameraChangeEmitter,
|
||||
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
||||
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||
}, bottom = extInsets),
|
||||
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||
)
|
||||
Searcher(
|
||||
state = searchState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
expanded = searchExpandedState,
|
||||
onExpandedChange = {
|
||||
searchExpandedState = it
|
||||
if (it)
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
},
|
||||
)
|
||||
|
||||
PredictiveBackHandler(scaffoldState.bottomSheetState.currentValue != SheetValue.Hidden) { progress ->
|
||||
sheetSwipeEnabled = false
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded) {
|
||||
peekHeightMultiplier = 1F - backEvent.progress
|
||||
}
|
||||
}
|
||||
if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded)
|
||||
scope.launch {
|
||||
scaffoldState.bottomSheetState.hide()
|
||||
peekHeightMultiplier = 1F
|
||||
viewModel.handleEvent(MapScreenEvent.DismissState)
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
peekHeightMultiplier = 1F
|
||||
}
|
||||
sheetSwipeEnabled = true
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.Companion.windowInsetsPadding(
|
||||
WindowInsets.Companion.safeContent.add(
|
||||
WindowInsets(bottom = extInsets)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Companion.BottomEnd
|
||||
) {
|
||||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
onClick = { viewModel.centreCameraToLocation() },
|
||||
) {
|
||||
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package moe.lava.banksia.ui
|
||||
package moe.lava.banksia.ui.screens
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -13,9 +13,12 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.client.repository.RouteRepository
|
||||
import moe.lava.banksia.client.repository.StopRepository
|
||||
import moe.lava.banksia.data.ptv.PtvService
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.Route
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.ui.components.getUIProperties
|
||||
import moe.lava.banksia.ui.platform.maps.CameraPosition
|
||||
import moe.lava.banksia.ui.platform.maps.CameraPositionBounds
|
||||
|
|
@ -32,23 +35,27 @@ import moe.lava.banksia.util.log
|
|||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
sealed class BanksiaEvent {
|
||||
data object DismissState : BanksiaEvent()
|
||||
sealed class MapScreenEvent {
|
||||
data object DismissState : MapScreenEvent()
|
||||
|
||||
data class SelectRoute(val id: Int?) : BanksiaEvent()
|
||||
data class SelectRun(val ref: String?) : BanksiaEvent()
|
||||
data class SelectStop(val typeAndId: Pair<PtvRouteType, Int>) : BanksiaEvent()
|
||||
data class SelectRoute(val id: String?) : MapScreenEvent()
|
||||
data class SelectRun(val ref: String?) : MapScreenEvent()
|
||||
data class SelectStop(val typeIdPair: Pair<RouteType, String>?) : MapScreenEvent()
|
||||
|
||||
data class SearchUpdate(val text: String) : BanksiaEvent()
|
||||
data class SearchUpdate(val text: String) : MapScreenEvent()
|
||||
}
|
||||
|
||||
data class InternalState(
|
||||
val route: Int? = null,
|
||||
val stop: Pair<PtvRouteType, Int>? = null,
|
||||
val route: String? = null,
|
||||
val stop: Pair<RouteType, String>? = null,
|
||||
val run: String? = null,
|
||||
)
|
||||
|
||||
class BanksiaViewModel : ViewModel() {
|
||||
class MapScreenViewModel(
|
||||
private val ptvService: PtvService,
|
||||
private val routeRepository: RouteRepository,
|
||||
private val stopRepository: StopRepository,
|
||||
) : ViewModel() {
|
||||
private var state = InternalState()
|
||||
set(value) {
|
||||
val last = field
|
||||
|
|
@ -72,7 +79,6 @@ class BanksiaViewModel : ViewModel() {
|
|||
private val iSearchState = MutableStateFlow(SearchState())
|
||||
val searchState = iSearchState.asStateFlow()
|
||||
|
||||
private val ptvService = PtvService(viewModelScope)
|
||||
private var locationTrackerJob: Job? = null
|
||||
private var lastKnownLocation: Point? = null
|
||||
|
||||
|
|
@ -80,14 +86,14 @@ class BanksiaViewModel : ViewModel() {
|
|||
viewModelScope.launch { searchUpdate("") }
|
||||
}
|
||||
|
||||
fun handleEvent(event: BanksiaEvent) {
|
||||
fun handleEvent(event: MapScreenEvent) {
|
||||
viewModelScope.launch {
|
||||
when (event) {
|
||||
is BanksiaEvent.DismissState -> dismissState()
|
||||
is BanksiaEvent.SelectRoute -> state = InternalState(route = event.id)
|
||||
is BanksiaEvent.SelectRun -> state = state.copy(run = event.ref, stop = null)
|
||||
is BanksiaEvent.SelectStop -> state = state.copy(stop = event.typeAndId, run = null)
|
||||
is BanksiaEvent.SearchUpdate -> searchUpdate(event.text)
|
||||
is MapScreenEvent.DismissState -> dismissState()
|
||||
is MapScreenEvent.SelectRoute -> state = InternalState(route = event.id)
|
||||
is MapScreenEvent.SelectRun -> state = state.copy(run = event.ref, stop = null)
|
||||
is MapScreenEvent.SelectStop -> state = state.copy(stop = event.typeIdPair, run = null)
|
||||
is MapScreenEvent.SearchUpdate -> searchUpdate(event.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +105,11 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun centreCameraToLocation() {
|
||||
viewModelScope.launch {
|
||||
log("msvm", "getting..")
|
||||
val routes = routeRepository.getAll()
|
||||
log("msvm", routes.joinToString("\n"))
|
||||
}
|
||||
lastKnownLocation?.let { location ->
|
||||
viewModelScope.launch {
|
||||
log("bvm", "emitting $location")
|
||||
|
|
@ -117,46 +128,48 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private suspend fun searchUpdate(text: String) {
|
||||
val entries = ptvService.routes()
|
||||
iSearchState.update { it.copy(text = text) }
|
||||
val entries = routeRepository.getAll()
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it.gtfsSubType()?.ordinal },
|
||||
{ it.routeNumber.toIntOrNull() },
|
||||
{ it.routeName }
|
||||
{ it.type.ordinal },
|
||||
{ it.number },
|
||||
{ it.name }
|
||||
)
|
||||
)
|
||||
.filter { it.routeNumber.contains(text) || it.routeName.lowercase().contains(text.lowercase()) }
|
||||
.filter { (it.number ?: "").contains(text) || it.name.lowercase().contains(text.lowercase()) }
|
||||
.map { route ->
|
||||
val (main, sub) = if (route.routeNumber.isNotEmpty()) {
|
||||
route.routeNumber to route.routeName
|
||||
val (main, sub) = if (route.number?.isNotEmpty() == true) {
|
||||
route.number to route.name
|
||||
} else {
|
||||
route.routeName to null
|
||||
route.name to null
|
||||
}
|
||||
|
||||
SearchState.SearchEntry(main, sub, route.routeId, route.routeType)
|
||||
SearchState.SearchEntry(main!!, sub, route.id, route.type)
|
||||
}
|
||||
|
||||
iSearchState.update { SearchState(entries, text) }
|
||||
}
|
||||
|
||||
private suspend fun switchRoute(routeId: Int?) {
|
||||
private suspend fun switchRoute(routeId: String?) {
|
||||
iMapState.update { MapState() }
|
||||
if (routeId == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
|
||||
val route = ptvService.route(routeId)
|
||||
val route = routeRepository.get(routeId)
|
||||
// val gtfsRoute = ptvService.route(routeId)
|
||||
iInfoState.update {
|
||||
InfoPanelState.Route(
|
||||
name = route.routeName,
|
||||
type = route.routeType,
|
||||
name = route.name,
|
||||
type = route.type,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch { buildPolylines(route) }
|
||||
// viewModelScope.launch { buildPolylines(gtfsRoute) }
|
||||
viewModelScope.launch { buildStops(route) }
|
||||
buildRuns(route)
|
||||
// buildRuns(gtfsRoute)
|
||||
}
|
||||
|
||||
private fun switchRun(ref: String?) {
|
||||
|
|
@ -175,7 +188,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
iInfoState.update {
|
||||
InfoPanelState.Run(
|
||||
direction = run.destinationName,
|
||||
type = run.routeType,
|
||||
type = RouteType.MetroTrain, // XXX HACK TODO FIXME
|
||||
)
|
||||
}
|
||||
routeName = ptvService.route(run.routeId).routeName
|
||||
|
|
@ -184,7 +197,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
iInfoState.update {
|
||||
InfoPanelState.Run(
|
||||
direction = run.destinationName,
|
||||
type = run.routeType,
|
||||
type = RouteType.MetroTrain, // FIXME HACK XXX TODO
|
||||
routeName = routeName,
|
||||
)
|
||||
}
|
||||
|
|
@ -193,25 +206,27 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
// [TODO]: Cleanup
|
||||
private suspend fun switchStop(typeAndId: Pair<PtvRouteType, Int>?) {
|
||||
if (typeAndId == null) {
|
||||
private suspend fun switchStop(pair: Pair<RouteType, String>?) {
|
||||
if (pair == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
val (routeType, stopId) = typeAndId
|
||||
val stop = ptvService.stop(routeType, stopId)
|
||||
val split = stop.stopName.split("/")
|
||||
val (type, id) = pair
|
||||
|
||||
val stop = stopRepository.get(id)
|
||||
// val stop = ptvService.stop(routeType, stopId)
|
||||
val split = stop.name.split("/")
|
||||
val name = split[0]
|
||||
val subname = split.getOrNull(1)
|
||||
iInfoState.update {
|
||||
InfoPanelState.Stop(
|
||||
id = stop.stopId,
|
||||
id = stop.id,
|
||||
name = name,
|
||||
subname = subname,
|
||||
)
|
||||
}
|
||||
|
||||
val res = ptvService.departures(stop.routeType, stop.stopId)
|
||||
val res = ptvService.departures(type, stop.id)
|
||||
// Map<
|
||||
// Pair<DirectionId, RouteId>,
|
||||
// Pair<DirectionName, List<DepartureTimes>>
|
||||
|
|
@ -285,7 +300,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
ptvService
|
||||
.runsFlow(route.routeId)
|
||||
.waitUntilSubscribed(iInfoState)
|
||||
.takeWhile { state.route == route.routeId }
|
||||
// .takeWhile { state.route == route.routeId }
|
||||
.onEach { runs ->
|
||||
val markers = runs
|
||||
.filter { it.vehiclePosition != null }
|
||||
|
|
@ -295,7 +310,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
Marker.Vehicle(
|
||||
Point(pos.latitude, pos.longitude),
|
||||
ref = run.runRef,
|
||||
type = route.routeType,
|
||||
type = RouteType.MetroTrain, // HACK TODO XXX FIXME
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -305,18 +320,17 @@ class BanksiaViewModel : ViewModel() {
|
|||
|
||||
}
|
||||
|
||||
private suspend fun buildStops(route: PtvRoute) {
|
||||
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
||||
val colour = route.routeType.getUIProperties().colour
|
||||
private suspend fun buildStops(route: Route) {
|
||||
val stops = stopRepository.getByRoute(route.id)
|
||||
val colour = route.type.getUIProperties().colour
|
||||
|
||||
val markers = stops
|
||||
.filter { it.stopLatitude != null && it.stopLongitude != null }
|
||||
.map { stop ->
|
||||
Marker.Stop(
|
||||
point = Point(stop.stopLatitude!!, stop.stopLongitude!!),
|
||||
id = stop.stopId,
|
||||
point = stop.pos,
|
||||
id = stop.id,
|
||||
colour = colour,
|
||||
type = route.routeType,
|
||||
type = route.type,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package moe.lava.banksia.ui.state
|
||||
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
|
||||
sealed class InfoPanelState {
|
||||
abstract val loading: Boolean
|
||||
|
|
@ -11,21 +11,21 @@ sealed class InfoPanelState {
|
|||
|
||||
data class Route(
|
||||
val name: String,
|
||||
val type: PtvRouteType,
|
||||
val type: RouteType,
|
||||
) : InfoPanelState() {
|
||||
override val loading = false
|
||||
}
|
||||
|
||||
data class Run(
|
||||
val direction: String,
|
||||
val type: PtvRouteType,
|
||||
val type: RouteType,
|
||||
val routeName: String? = null,
|
||||
) : InfoPanelState() {
|
||||
override val loading = routeName == null
|
||||
}
|
||||
|
||||
data class Stop(
|
||||
val id: Int,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val subname: String? = null,
|
||||
val departures: List<Departure>? = null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package moe.lava.banksia.ui.state
|
||||
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
|
||||
data class SearchState(
|
||||
val entries: List<SearchEntry> = listOf(),
|
||||
|
|
@ -9,7 +9,7 @@ data class SearchState(
|
|||
data class SearchEntry(
|
||||
val mainText: String,
|
||||
val subText: String?,
|
||||
val routeId: Int,
|
||||
val routeType: PtvRouteType,
|
||||
val routeId: String,
|
||||
val routeType: RouteType,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import moe.lava.banksia.util.Point
|
||||
|
|
@ -23,7 +23,7 @@ actual fun getScreenHeight(): Int {
|
|||
actual fun Maps(
|
||||
modifier: Modifier,
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ koin = "4.1.0"
|
|||
kotlin = "2.2.0"
|
||||
kotlinxDatetime = "0.7.1"
|
||||
kotlinxSerializationCsv = "0.2.18"
|
||||
kotlinxSerializationJson = "1.9.0"
|
||||
kotlinxSerialization = "1.9.0"
|
||||
ksp = "2.2.0-2.0.2"
|
||||
ktor = "3.2.3"
|
||||
logback = "1.5.18"
|
||||
|
|
@ -39,23 +39,29 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver
|
|||
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
||||
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
|
||||
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
|
||||
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||
kotlinx-serialization-csv = { module = "com.lightningkite:kotlinx-serialization-csv-durable", version.ref = "kotlinxSerializationCsv" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinxSerialization" }
|
||||
ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
||||
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
ktor-server-contentnegotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
|
||||
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
|
||||
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
|
||||
ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
|
||||
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
|
||||
maps-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "mapsCompose" }
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
|
||||
play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" }
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ dependencies {
|
|||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.ktor)
|
||||
implementation(libs.kotlinx.serialization.csv)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.ktor.client.contentnegotiation)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.ktor.server.contentnegotiation)
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.netty)
|
||||
implementation(libs.room.runtime)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
package moe.lava.banksia.server
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.application.log
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.netty.Netty
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.routing
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moe.lava.banksia.di.CommonModules
|
||||
import moe.lava.banksia.room.dao.RouteDao
|
||||
import moe.lava.banksia.room.dao.StopDao
|
||||
import moe.lava.banksia.server.di.ServerModules
|
||||
import moe.lava.banksia.server.gtfs.GtfsHandler
|
||||
import org.koin.dsl.module
|
||||
|
|
@ -24,6 +31,9 @@ fun main() {
|
|||
}
|
||||
|
||||
fun Application.module() {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
install(Koin) {
|
||||
modules(module { single { log } })
|
||||
modules(CommonModules, ServerModules)
|
||||
|
|
@ -40,5 +50,66 @@ fun Application.module() {
|
|||
handler.update(datasetUrl)
|
||||
}
|
||||
}
|
||||
|
||||
get("/routes") {
|
||||
val routes = withContext(context = Dispatchers.IO) {
|
||||
inject<RouteDao>().value.getAll()
|
||||
}
|
||||
val res = routes.map { it.asModel() }
|
||||
call.respond(res)
|
||||
}
|
||||
get("/routes/{route_id}") {
|
||||
val routeId = call.parameters["route_id"]!!
|
||||
val route = withContext(context = Dispatchers.IO) {
|
||||
inject<RouteDao>().value.get(routeId)
|
||||
}
|
||||
if (route != null)
|
||||
call.respond(route.asModel())
|
||||
else
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
get("/stops") {
|
||||
val routes = withContext(context = Dispatchers.IO) {
|
||||
inject<StopDao>().value.getAll()
|
||||
}
|
||||
val res = routes.map { it.asModel() }
|
||||
call.respond(res)
|
||||
}
|
||||
get("/stops/{stop_id}") {
|
||||
val stopId = call.parameters["stop_id"]!!
|
||||
val stop = withContext(context = Dispatchers.IO) {
|
||||
inject<StopDao>().value.get(stopId)
|
||||
}
|
||||
if (stop != null)
|
||||
call.respond(stop.asModel())
|
||||
else
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
get("/route_stops/{route_id}") {
|
||||
val routeId = call.parameters["route_id"]!!
|
||||
val useParent = call.queryParameters["parent"] in listOf("true", "1")
|
||||
val stops = withContext(Dispatchers.IO) {
|
||||
val routeDao by inject<RouteDao>()
|
||||
if (useParent)
|
||||
routeDao.stopsParent(routeId)
|
||||
else
|
||||
routeDao.stops(routeId)
|
||||
}
|
||||
call.respond(stops.map { it.asModel() })
|
||||
// val stops = withContext(Dispatchers.IO) {
|
||||
// val stopDao by inject<StopDao>()
|
||||
// val stopTimeDao by inject<StopTimeDao>()
|
||||
// val tripDao by inject<TripDao>()
|
||||
//
|
||||
// tripDao.getByRoute(routeId)
|
||||
// .map { it.id }
|
||||
// .let { stopTimeDao.get(it) }
|
||||
// .flatMap { it.asModel().stopInfos }
|
||||
// .map { it.stopId }
|
||||
// .let { stopDao.get(it) }
|
||||
// .map { it.asModel() }
|
||||
// }
|
||||
// call.respond(stops)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,18 @@ import io.ktor.utils.io.copyAndClose
|
|||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.modules.EmptySerializersModule
|
||||
import moe.lava.banksia.model.Route
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.model.Shape
|
||||
import moe.lava.banksia.room.dao.RouteDao
|
||||
import moe.lava.banksia.room.dao.ShapeDao
|
||||
import moe.lava.banksia.model.Stop
|
||||
import moe.lava.banksia.model.StopTime
|
||||
import moe.lava.banksia.model.Trip
|
||||
import moe.lava.banksia.room.Database
|
||||
import moe.lava.banksia.room.converter.RouteTypeConverter
|
||||
import moe.lava.banksia.room.entity.asEntity
|
||||
import moe.lava.banksia.server.gtfs.structures.GtfsRoute
|
||||
import moe.lava.banksia.server.gtfs.structures.GtfsShape
|
||||
import moe.lava.banksia.server.gtfs.structures.GtfsStop
|
||||
import moe.lava.banksia.server.gtfs.structures.GtfsStopTime
|
||||
import moe.lava.banksia.server.gtfs.structures.GtfsTrip
|
||||
import moe.lava.banksia.util.Point
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
|
@ -25,9 +31,7 @@ import java.util.zip.ZipFile
|
|||
class GtfsHandler(
|
||||
private val log: Logger,
|
||||
private val client: HttpClient,
|
||||
|
||||
private val routeDao: RouteDao,
|
||||
private val shapeDao: ShapeDao,
|
||||
private val db: Database,
|
||||
) {
|
||||
private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule()))
|
||||
private val datasetPath = File("/tmp/banksia", "dataset.zip")
|
||||
|
|
@ -49,27 +53,30 @@ class GtfsHandler(
|
|||
}
|
||||
|
||||
log.info("extracting...")
|
||||
val files = extractAll(datasetPath)
|
||||
// val files = extractAll(datasetPath)
|
||||
val files = datasetPath.parentFile
|
||||
.listFiles { it.isDirectory }
|
||||
.flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() }
|
||||
|
||||
addRoutes(files)
|
||||
addStops(files)
|
||||
addShapes(files)
|
||||
addTrips(files)
|
||||
addStopTimes(files)
|
||||
|
||||
log.info("done!")
|
||||
}
|
||||
|
||||
private suspend fun addRoutes(files: List<File>) {
|
||||
val dao = db.routeDao
|
||||
log.info("parsing routes...")
|
||||
val routes = files
|
||||
.filter { it.name == "routes.txt" }
|
||||
.flatMap { fd -> parseRoutes(fd) }
|
||||
|
||||
log.info("inserting routes...")
|
||||
routeDao.deleteAll()
|
||||
routeDao.insertAll(*routes.toTypedArray())
|
||||
|
||||
log.info("parsing shapes...")
|
||||
val shapes = files
|
||||
.filter { it.name == "shapes.txt" }
|
||||
.flatMap { fd -> parseShapes(fd) }
|
||||
|
||||
log.info("inserting shapes...")
|
||||
shapeDao.deleteAll()
|
||||
shapeDao.insertAll(*shapes.toTypedArray())
|
||||
|
||||
log.info("done!")
|
||||
dao.deleteAll()
|
||||
dao.insertAll(*routes.map { it.asEntity() }.toTypedArray())
|
||||
}
|
||||
|
||||
private fun parseRoutes(fd: File) =
|
||||
|
|
@ -77,12 +84,24 @@ class GtfsHandler(
|
|||
.map { with(it) {
|
||||
Route(
|
||||
id = route_id,
|
||||
type = RouteType.from(fd.parentFile.name.toInt()),
|
||||
type = RouteTypeConverter.from(fd.parentFile.name.toInt()),
|
||||
number = route_short_name,
|
||||
name = route_long_name,
|
||||
)
|
||||
} }
|
||||
|
||||
private suspend fun addShapes(files: List<File>) {
|
||||
val dao = db.shapeDao
|
||||
log.info("parsing shapes...")
|
||||
val shapes = files
|
||||
.filter { it.name == "shapes.txt" }
|
||||
.flatMap { fd -> parseShapes(fd) }
|
||||
|
||||
log.info("inserting shapes...")
|
||||
dao.deleteAll()
|
||||
dao.insertAll(*shapes.map { it.asEntity() }.toTypedArray())
|
||||
}
|
||||
|
||||
private fun parseShapes(fd: File) =
|
||||
fd.parseCsv<GtfsShape>()
|
||||
.groupBy { it.shape_id }
|
||||
|
|
@ -94,6 +113,95 @@ class GtfsHandler(
|
|||
Shape(id, points)
|
||||
}
|
||||
|
||||
private suspend fun addStops(files: List<File>) {
|
||||
val dao = db.stopDao
|
||||
log.info("parsing stops...")
|
||||
val stops = files
|
||||
.filter { it.name == "stops.txt" }
|
||||
.flatMap { fd -> parseStops(fd) }
|
||||
|
||||
log.info("inserting stops...")
|
||||
dao.deleteAll()
|
||||
stops
|
||||
.groupBy { it.id }
|
||||
.forEach { (id, gstops) ->
|
||||
if (gstops.size > 1) {
|
||||
// if (gstops.withIndex().any { (i, stop) -> i != 0 && stop == gstops[i - 1] })
|
||||
gstops.forEach {
|
||||
log.info("duplicate $id: $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray())
|
||||
}
|
||||
|
||||
private fun parseStops(fd: File) =
|
||||
fd.parseCsv<GtfsStop>()
|
||||
.map { with(it) {
|
||||
Stop(
|
||||
id = stop_id,
|
||||
name = stop_name,
|
||||
pos = Point(stop_lat, stop_lon),
|
||||
parent = parent_station,
|
||||
hasWheelChairBoarding = wheelchair_boarding == "1",
|
||||
level = level_id,
|
||||
platformCode = platform_code,
|
||||
)
|
||||
} }
|
||||
|
||||
private suspend fun addStopTimes(files: List<File>) {
|
||||
val dao = db.stopTimeDao
|
||||
log.info("parsing stop times...")
|
||||
val stopTimes = files
|
||||
.filter { it.name == "stop_times.txt" }
|
||||
.flatMap { fd -> parseStopTimes(fd) }
|
||||
|
||||
log.info("inserting stop times...")
|
||||
dao.deleteAll()
|
||||
dao.insertOrReplaceAll(*stopTimes.map { it.asEntity() }.toTypedArray())
|
||||
}
|
||||
|
||||
private fun parseStopTimes(fd: File) =
|
||||
fd.parseCsv<GtfsStopTime>()
|
||||
.map { with(it) {
|
||||
StopTime(
|
||||
tripId = trip_id,
|
||||
stopId = stop_id,
|
||||
arrivalTime = GtfsStopTime.parseGtfsTime(arrival_time),
|
||||
departureTime = GtfsStopTime.parseGtfsTime(departure_time),
|
||||
headsign = stop_headsign,
|
||||
pickupType = pickup_type,
|
||||
dropOffType = drop_off_type,
|
||||
)
|
||||
} }
|
||||
|
||||
|
||||
private suspend fun addTrips(files: List<File>) {
|
||||
val dao = db.tripDao
|
||||
log.info("parsing trips...")
|
||||
val trips = files
|
||||
.filter { it.name == "trips.txt" }
|
||||
.flatMap { fd -> parseTrips(fd) }
|
||||
|
||||
log.info("inserting trips...")
|
||||
dao.deleteAll()
|
||||
dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray())
|
||||
}
|
||||
|
||||
private fun parseTrips(fd: File) =
|
||||
fd.parseCsv<GtfsTrip>()
|
||||
.map { with(it) {
|
||||
Trip(
|
||||
id = trip_id,
|
||||
routeId = route_id,
|
||||
serviceId = service_id,
|
||||
shapeId = shape_id.ifEmpty { null },
|
||||
tripHeadsign = trip_headsign,
|
||||
directionId = direction_id,
|
||||
blockId = block_id,
|
||||
wheelchairAccessible = wheelchair_accessible,
|
||||
)
|
||||
} }
|
||||
|
||||
private fun extract(fd: File): List<File> {
|
||||
val outputs = mutableListOf<File>()
|
||||
|
|
@ -114,7 +222,7 @@ class GtfsHandler(
|
|||
|
||||
private fun extractAll(fd: File) = extract(fd).flatMap(::extract)
|
||||
|
||||
private fun <T> File.parseCsv(): List<T> = this
|
||||
private inline fun <reified T> File.parseCsv(): List<T> = this
|
||||
.readText()
|
||||
.replace("\uFEFF", "") // remove bom
|
||||
.replace("\r\n", "\n") // crlf -> lf
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package moe.lava.banksia.server.gtfs.structures
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Suppress("PropertyName")
|
||||
@Serializable
|
||||
data class GtfsStop(
|
||||
val stop_id: String,
|
||||
val stop_name: String,
|
||||
val stop_lat: Double,
|
||||
val stop_lon: Double,
|
||||
val location_type: String,
|
||||
val parent_station: String,
|
||||
val wheelchair_boarding: String,
|
||||
val level_id: String,
|
||||
val platform_code: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package moe.lava.banksia.server.gtfs.structures
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.lava.banksia.model.FutureTime
|
||||
|
||||
@Suppress("PropertyName")
|
||||
@Serializable
|
||||
data class GtfsStopTime(
|
||||
val trip_id: String,
|
||||
val arrival_time: String,
|
||||
val departure_time: String,
|
||||
val stop_id: String,
|
||||
val stop_sequence: Int,
|
||||
val stop_headsign: String,
|
||||
val pickup_type: Int,
|
||||
val drop_off_type: Int,
|
||||
val shape_dist_traveled: String,
|
||||
) {
|
||||
companion object {
|
||||
fun parseGtfsTime(time: String): FutureTime {
|
||||
val (hour, minute, second) = time.split(":").map { it.toInt() }
|
||||
return FutureTime.from(hour, minute, second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package moe.lava.banksia.server.gtfs.structures
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Suppress("PropertyName")
|
||||
@Serializable
|
||||
data class GtfsTrip(
|
||||
val route_id: String,
|
||||
val service_id: String,
|
||||
val trip_id: String,
|
||||
val shape_id: String,
|
||||
val trip_headsign: String,
|
||||
val direction_id: String,
|
||||
val block_id: String,
|
||||
val wheelchair_accessible: String,
|
||||
)
|
||||
|
|
@ -37,6 +37,7 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
|
|
@ -48,6 +49,7 @@ kotlin {
|
|||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.serialization.protobuf)
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.sqlite.bundled)
|
||||
}
|
||||
|
|
|
|||
315
shared/schemas/moe.lava.banksia.room.Database/2.json
Normal file
315
shared/schemas/moe.lava.banksia.room.Database/2.json
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "83ece554400bb035c267dc2414c23293",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Route",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "number",
|
||||
"columnName": "number",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Shape",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "path",
|
||||
"columnName": "path",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Stop",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lat",
|
||||
"columnName": "lat",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lng",
|
||||
"columnName": "lng",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parent",
|
||||
"columnName": "parent",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasWheelChairBoarding",
|
||||
"columnName": "hasWheelChairBoarding",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "level",
|
||||
"columnName": "level",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "platformCode",
|
||||
"columnName": "platformCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Stop_parent",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"parent"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "StopTime",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "tripId",
|
||||
"columnName": "tripId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "stopId",
|
||||
"columnName": "stopId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "arrivalTime",
|
||||
"columnName": "arrivalTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "departureTime",
|
||||
"columnName": "departureTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "headsign",
|
||||
"columnName": "headsign",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pickupType",
|
||||
"columnName": "pickupType",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dropOffType",
|
||||
"columnName": "dropOffType",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"tripId",
|
||||
"stopId"
|
||||
]
|
||||
},
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Trip",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tripId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "Stop",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"stopId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "Trip",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "routeId",
|
||||
"columnName": "routeId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shapeId",
|
||||
"columnName": "shapeId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tripHeadsign",
|
||||
"columnName": "tripHeadsign",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "directionId",
|
||||
"columnName": "directionId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "blockId",
|
||||
"columnName": "blockId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wheelchairAccessible",
|
||||
"columnName": "wheelchairAccessible",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Trip_routeId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"routeId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Route",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"routeId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "Shape",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"shapeId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '83ece554400bb035c267dc2414c23293')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.room.RoomDatabase
|
|||
import moe.lava.banksia.room.Database
|
||||
import org.koin.core.parameter.ParametersHolder
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
|
||||
class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
|
||||
override fun getBuilder(): RoomDatabase.Builder<Database> {
|
||||
|
|
@ -19,4 +20,6 @@ class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
|
|||
}
|
||||
|
||||
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
|
||||
AndroidDatabaseBuilder(p.get())
|
||||
AndroidDatabaseBuilder(get())
|
||||
|
||||
internal actual val ExtPlatformModule = module { }
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import io.ktor.client.request.url
|
|||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.appendPathSegments
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -22,9 +21,10 @@ import moe.lava.banksia.data.ptv.structures.PtvDeparture
|
|||
import moe.lava.banksia.data.ptv.structures.PtvDirection
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRun
|
||||
import moe.lava.banksia.data.ptv.structures.PtvStop
|
||||
import moe.lava.banksia.util.CacheMap
|
||||
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
|
||||
|
|
@ -59,16 +59,15 @@ suspend inline fun <K, V> MutableMap<K, V>.getOrPutSuspend(key: K, defaultValue:
|
|||
return this[key]!!
|
||||
}
|
||||
|
||||
class PtvService(coroutineScope: CoroutineScope) {
|
||||
class PtvService() {
|
||||
class PtvCache(
|
||||
coroutineScope: CoroutineScope,
|
||||
val directions: CacheMap<Pair<Int, Int>, PtvDirection> = CacheMap(coroutineScope),
|
||||
val routes: CacheMap<Int, PtvRoute> = CacheMap(coroutineScope),
|
||||
val runs: CacheMap<String, PtvRun> = CacheMap(coroutineScope),
|
||||
val stops: CacheMap<Int, PtvStop> = CacheMap(coroutineScope),
|
||||
val directions: MutableMap<Pair<Int, Int>, PtvDirection> = mutableMapOf(),
|
||||
val routes: MutableMap<Int, PtvRoute> = mutableMapOf(),
|
||||
val runs: MutableMap<String, PtvRun> = mutableMapOf(),
|
||||
val stops: MutableMap<Int, PtvStop> = mutableMapOf(),
|
||||
)
|
||||
|
||||
val cache = PtvCache(coroutineScope)
|
||||
val cache = PtvCache()
|
||||
|
||||
private val client = HttpClient() {
|
||||
install(ContentNegotiation) {
|
||||
|
|
@ -227,6 +226,20 @@ class PtvService(coroutineScope: CoroutineScope) {
|
|||
return cache.directions[directionId to routeId]!!
|
||||
}
|
||||
|
||||
suspend fun departures(routeType: RouteType, stopId: String): Responses.PtvDeparturesResponse =
|
||||
client
|
||||
.safeGet ("departures") {
|
||||
url {
|
||||
appendPathSegments(
|
||||
"route_type", routeType.asPtvType().ordinal.toString(),
|
||||
"stop", stopId.toString(),
|
||||
)
|
||||
parameter("expand", "Route")
|
||||
parameter("expand", "Direction")
|
||||
parameter("gtfs", "true")
|
||||
}
|
||||
}.body()
|
||||
|
||||
suspend fun departures(routeType: PtvRouteType, stopId: Int): Responses.PtvDeparturesResponse =
|
||||
client
|
||||
.safeGet ("departures") {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import moe.lava.banksia.model.RouteType
|
||||
|
||||
private object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
|
||||
|
|
@ -30,4 +31,20 @@ enum class PtvRouteType {
|
|||
BUS,
|
||||
VLINE,
|
||||
NIGHT_BUS,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromModel(type: RouteType) = when (type) {
|
||||
RouteType.MetroTrain -> TRAIN
|
||||
RouteType.MetroTram -> TRAM
|
||||
RouteType.MetroBus -> BUS
|
||||
RouteType.RegionalTrain -> VLINE
|
||||
RouteType.RegionalCoach -> BUS
|
||||
RouteType.RegionalBus -> BUS
|
||||
RouteType.SkyBus -> BUS
|
||||
RouteType.Interstate -> TRAIN
|
||||
}
|
||||
|
||||
fun RouteType.asPtvType() = fromModel(this)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ val CommonModules = module {
|
|||
includes(PlatformModule)
|
||||
|
||||
single { Database.build(get<PlatformDatabaseBuilder>().getBuilder()) }
|
||||
single { get<Database>().getRouteDao() }
|
||||
single { get<Database>().getShapeDao() }
|
||||
single { get<Database>().routeDao }
|
||||
single { get<Database>().shapeDao }
|
||||
single { get<Database>().stopDao }
|
||||
single { get<Database>().stopTimeDao }
|
||||
single { get<Database>().tripDao }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package moe.lava.banksia.di
|
|||
|
||||
import androidx.room.RoomDatabase
|
||||
import moe.lava.banksia.room.Database
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.parameter.ParametersHolder
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
|
|
@ -12,6 +13,9 @@ interface PlatformDatabaseBuilder {
|
|||
|
||||
expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder
|
||||
|
||||
internal expect val ExtPlatformModule: Module
|
||||
|
||||
internal val PlatformModule = module {
|
||||
includes(ExtPlatformModule)
|
||||
single { provideDatabaseBuilder(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import moe.lava.banksia.model.FutureTime.Companion.asInt
|
||||
|
||||
@Serializable(FutureTimeSerialiser::class)
|
||||
data class FutureTime(
|
||||
val dayOffset: Boolean,
|
||||
val time: LocalTime,
|
||||
) {
|
||||
companion object {
|
||||
fun from(hour: Int, minute: Int, second: Int): FutureTime {
|
||||
var nHour = hour
|
||||
val nextDay = hour >= 24
|
||||
if (nextDay)
|
||||
nHour -= 24
|
||||
val time = LocalTime(nHour, minute, second)
|
||||
return FutureTime(nextDay, time)
|
||||
}
|
||||
|
||||
fun FutureTime.asInt() =
|
||||
trueHour * 3600 + minute * 60 + second
|
||||
|
||||
fun fromInt(int: Int) = FutureTime.from(
|
||||
int / 3600,
|
||||
(int / 60) % 60,
|
||||
int % 60,
|
||||
)
|
||||
}
|
||||
|
||||
val hour = time.hour
|
||||
val minute = time.minute
|
||||
val second = time.second
|
||||
val trueHour = time.hour + (if (dayOffset) 24 else 0)
|
||||
}
|
||||
|
||||
object FutureTimeSerialiser: KSerializer<FutureTime> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor(FutureTimeSerialiser::class.qualifiedName!!, PrimitiveKind.INT)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: FutureTime) = encoder.encodeInt(value.asInt())
|
||||
override fun deserialize(decoder: Decoder) = FutureTime.fromInt(decoder.decodeInt())
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Entity
|
||||
@Serializable
|
||||
data class Route(
|
||||
@PrimaryKey val id: String,
|
||||
val id: String,
|
||||
val type: RouteType,
|
||||
val number: String?,
|
||||
val name: String,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class RouteType(val value: Int) {
|
||||
MetroTrain(2),
|
||||
MetroTram(3),
|
||||
|
|
@ -12,12 +13,4 @@ enum class RouteType(val value: Int) {
|
|||
SkyBus(11),
|
||||
Interstate(10),
|
||||
;
|
||||
|
||||
companion object {
|
||||
@TypeConverter
|
||||
fun from(value: Int) = RouteType.entries.first { it.value == value }
|
||||
|
||||
@TypeConverter
|
||||
fun to(routeType: RouteType) = routeType.value
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
data class Run(
|
||||
val ref: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Service(
|
||||
val id: String,
|
||||
val days: List<DayOfWeek>,
|
||||
val start: LocalDate,
|
||||
val end: LocalDate,
|
||||
)
|
||||
|
|
@ -1,16 +1,12 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import moe.lava.banksia.room.converter.ShapeConverter
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
typealias ShapePath = List<Point>
|
||||
|
||||
@Entity
|
||||
@TypeConverters(ShapeConverter::class)
|
||||
@Serializable
|
||||
data class Shape(
|
||||
@PrimaryKey val id: String,
|
||||
val id: String,
|
||||
val path: ShapePath,
|
||||
)
|
||||
|
|
|
|||
15
shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt
Normal file
15
shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
@Serializable
|
||||
data class Stop(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val pos: Point,
|
||||
val parent: String,
|
||||
val hasWheelChairBoarding: Boolean,
|
||||
val level: String,
|
||||
val platformCode: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StopTime(
|
||||
val tripId: String,
|
||||
val stopId: String,
|
||||
val arrivalTime: FutureTime,
|
||||
val departureTime: FutureTime,
|
||||
val headsign: String?,
|
||||
val pickupType: Int,
|
||||
val dropOffType: Int,
|
||||
)
|
||||
15
shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt
Normal file
15
shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package moe.lava.banksia.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Trip(
|
||||
val id: String,
|
||||
val routeId: String,
|
||||
val serviceId: String,
|
||||
val shapeId: String?,
|
||||
val tripHeadsign: String,
|
||||
val directionId: String,
|
||||
val blockId: String,
|
||||
val wheelchairAccessible: String,
|
||||
)
|
||||
|
|
@ -1,28 +1,51 @@
|
|||
package moe.lava.banksia.room
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import moe.lava.banksia.model.Route
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.model.Shape
|
||||
import moe.lava.banksia.room.converter.RouteTypeConverter
|
||||
import moe.lava.banksia.room.dao.RouteDao
|
||||
import moe.lava.banksia.room.dao.ShapeDao
|
||||
import moe.lava.banksia.room.dao.StopDao
|
||||
import moe.lava.banksia.room.dao.StopTimeDao
|
||||
import moe.lava.banksia.room.dao.TripDao
|
||||
import moe.lava.banksia.room.entity.RouteEntity
|
||||
import moe.lava.banksia.room.entity.ShapeEntity
|
||||
import moe.lava.banksia.room.entity.StopEntity
|
||||
import moe.lava.banksia.room.entity.StopTimeEntity
|
||||
import moe.lava.banksia.room.entity.TripEntity
|
||||
import androidx.room.Database as DatabaseAnnotation
|
||||
|
||||
@DatabaseAnnotation(entities = [Route::class, Shape::class], version = 1)
|
||||
@TypeConverters(RouteType.Companion::class)
|
||||
@DatabaseAnnotation(
|
||||
version = 2,
|
||||
entities = [
|
||||
RouteEntity::class,
|
||||
ShapeEntity::class,
|
||||
StopEntity::class,
|
||||
StopTimeEntity::class,
|
||||
TripEntity::class,
|
||||
],
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 1, to = 2),
|
||||
]
|
||||
)
|
||||
@TypeConverters(RouteTypeConverter::class)
|
||||
abstract class Database : RoomDatabase() {
|
||||
abstract fun getRouteDao(): RouteDao
|
||||
abstract fun getShapeDao(): ShapeDao
|
||||
abstract val routeDao: RouteDao
|
||||
abstract val shapeDao: ShapeDao
|
||||
abstract val stopDao: StopDao
|
||||
abstract val stopTimeDao: StopTimeDao
|
||||
abstract val tripDao: TripDao
|
||||
|
||||
companion object {
|
||||
fun build(base: Builder<Database>) =
|
||||
base.fallbackToDestructiveMigrationOnDowngrade(true)
|
||||
base.fallbackToDestructiveMigration(true)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.setQueryCoroutineContext(Dispatchers.IO)
|
||||
// .fallbackToDestructiveMigration(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package moe.lava.banksia.room.converter
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import moe.lava.banksia.model.RouteType
|
||||
|
||||
object RouteTypeConverter {
|
||||
@TypeConverter
|
||||
fun from(value: Int) = RouteType.entries.first { it.value == value }
|
||||
|
||||
@TypeConverter
|
||||
fun to(routeType: RouteType) = routeType.value
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import androidx.room.TypeConverter
|
|||
import moe.lava.banksia.model.ShapePath
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
object ShapeConverter {
|
||||
object ShapePathConverter {
|
||||
@TypeConverter
|
||||
fun from(value: ByteArray): ShapePath {
|
||||
return value
|
||||
|
|
@ -3,23 +3,47 @@ package moe.lava.banksia.room.dao
|
|||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||
import androidx.room.Query
|
||||
import moe.lava.banksia.model.Route
|
||||
import moe.lava.banksia.room.entity.RouteEntity
|
||||
import moe.lava.banksia.room.entity.StopEntity
|
||||
|
||||
@Dao
|
||||
interface RouteDao {
|
||||
@Query("SELECT * FROM Route")
|
||||
suspend fun getAll(): List<Route>
|
||||
suspend fun getAll(): List<RouteEntity>
|
||||
|
||||
@Query("SELECT * FROM Route WHERE id == :id")
|
||||
suspend fun get(id: String): Route?
|
||||
suspend fun get(id: String): RouteEntity?
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(vararg routes: Route)
|
||||
suspend fun insertAll(vararg routes: RouteEntity)
|
||||
|
||||
@Insert(onConflict = REPLACE)
|
||||
suspend fun insertOrReplaceAll(vararg routes: RouteEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(route: Route)
|
||||
suspend fun delete(route: RouteEntity)
|
||||
|
||||
@Query("DELETE FROM Route")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Query("""
|
||||
SELECT Stop.* FROM Stop
|
||||
INNER JOIN StopTime ON StopTime.stopId == Stop.id
|
||||
INNER JOIN Trip ON Trip.id == StopTime.tripId
|
||||
WHERE Trip.routeId == :id
|
||||
GROUP BY Stop.id
|
||||
""")
|
||||
suspend fun stops(id: String): List<StopEntity>
|
||||
|
||||
@Query("""
|
||||
SELECT Stop.* FROM Stop
|
||||
INNER JOIN Stop Child ON Child.parent == Stop.id
|
||||
INNER JOIN StopTime ON StopTime.stopId == Child.id
|
||||
INNER JOIN Trip ON Trip.id == StopTime.tripId
|
||||
WHERE Trip.routeId == :id
|
||||
GROUP BY Stop.id
|
||||
""")
|
||||
suspend fun stopsParent(id: String): List<StopEntity>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ import androidx.room.Dao
|
|||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import moe.lava.banksia.model.Shape
|
||||
import moe.lava.banksia.room.entity.ShapeEntity
|
||||
|
||||
@Dao
|
||||
interface ShapeDao {
|
||||
@Query("SELECT * FROM Shape WHERE id == :id")
|
||||
suspend fun get(id: String): Shape?
|
||||
suspend fun get(id: String): ShapeEntity?
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(vararg shapes: Shape)
|
||||
suspend fun insertAll(vararg shapes: ShapeEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(shape: Shape)
|
||||
suspend fun delete(shape: ShapeEntity)
|
||||
|
||||
@Query("DELETE FROM Shape")
|
||||
suspend fun deleteAll()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package moe.lava.banksia.room.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||
import androidx.room.Query
|
||||
import moe.lava.banksia.room.entity.StopEntity
|
||||
|
||||
@Dao
|
||||
interface StopDao {
|
||||
@Query("SELECT * FROM Stop")
|
||||
suspend fun getAll(): List<StopEntity>
|
||||
|
||||
@Query("SELECT * FROM Stop WHERE id == :id")
|
||||
suspend fun get(id: String): StopEntity?
|
||||
|
||||
@Query("SELECT * FROM Stop WHERE id IN (:ids)")
|
||||
suspend fun get(ids: List<String>): List<StopEntity>
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(vararg stops: StopEntity)
|
||||
|
||||
@Insert(onConflict = REPLACE)
|
||||
suspend fun insertOrReplaceAll(vararg stops: StopEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(stop: StopEntity)
|
||||
|
||||
@Query("DELETE FROM Stop")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package moe.lava.banksia.room.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||
import androidx.room.Query
|
||||
import moe.lava.banksia.room.entity.StopTimeEntity
|
||||
|
||||
@Dao
|
||||
interface StopTimeDao {
|
||||
@Query("SELECT * FROM StopTime")
|
||||
suspend fun getAll(): List<StopTimeEntity>
|
||||
|
||||
@Query("SELECT * FROM StopTime WHERE tripId == :tripId")
|
||||
suspend fun get(tripId: String): StopTimeEntity?
|
||||
|
||||
@Query("SELECT * FROM StopTime WHERE tripId IN (:tripIds)")
|
||||
suspend fun get(tripIds: List<String>): List<StopTimeEntity>
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(vararg stopTimes: StopTimeEntity)
|
||||
|
||||
@Insert(onConflict = REPLACE)
|
||||
suspend fun insertOrReplaceAll(vararg stopTimes: StopTimeEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(stopTime: StopTimeEntity)
|
||||
|
||||
@Query("DELETE FROM StopTime")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package moe.lava.banksia.room.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||
import androidx.room.Query
|
||||
import moe.lava.banksia.room.entity.TripEntity
|
||||
|
||||
@Dao
|
||||
interface TripDao {
|
||||
@Query("SELECT * FROM Trip")
|
||||
suspend fun getAll(): List<TripEntity>
|
||||
|
||||
@Query("SELECT * FROM Trip WHERE id == :id")
|
||||
suspend fun get(id: String): TripEntity?
|
||||
|
||||
@Query("SELECT * FROM Trip WHERE routeId == :id")
|
||||
suspend fun getByRoute(id: String): List<TripEntity>
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(vararg trips: TripEntity)
|
||||
|
||||
@Insert(onConflict = REPLACE)
|
||||
suspend fun insertOrReplaceAll(vararg trips: TripEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(trip: TripEntity)
|
||||
|
||||
@Query("DELETE FROM Trip")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package moe.lava.banksia.room.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import moe.lava.banksia.model.Route
|
||||
import moe.lava.banksia.model.RouteType
|
||||
|
||||
@Entity("Route")
|
||||
data class RouteEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val type: RouteType,
|
||||
val number: String?,
|
||||
val name: String,
|
||||
) {
|
||||
fun asModel() = Route(id, type, number, name)
|
||||
}
|
||||
|
||||
fun Route.asEntity() = RouteEntity(id, type, number, name)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package moe.lava.banksia.room.entity
|
||||
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import moe.lava.banksia.model.Service
|
||||
|
||||
data class ServiceEntity(
|
||||
val id: String,
|
||||
val days: Int,
|
||||
val start: Int,
|
||||
val end: Int,
|
||||
) {
|
||||
object Parser {
|
||||
private fun Int.check(other: Int) = (this and other) != 0
|
||||
|
||||
fun deserialiseDays(days: Int): List<DayOfWeek> = buildList {
|
||||
if (days.check(1))
|
||||
add(DayOfWeek.MONDAY)
|
||||
if (days.check(1 shl 1))
|
||||
add(DayOfWeek.TUESDAY)
|
||||
if (days.check(1 shl 2))
|
||||
add(DayOfWeek.WEDNESDAY)
|
||||
if (days.check(1 shl 3))
|
||||
add(DayOfWeek.THURSDAY)
|
||||
if (days.check(1 shl 4))
|
||||
add(DayOfWeek.FRIDAY)
|
||||
if (days.check(1 shl 5))
|
||||
add(DayOfWeek.SATURDAY)
|
||||
if (days.check(1 shl 6))
|
||||
add(DayOfWeek.SUNDAY)
|
||||
}
|
||||
fun serialiseDays(days: List<DayOfWeek>): Int =
|
||||
days.fold(0) { vl, n ->
|
||||
vl + when (n) {
|
||||
DayOfWeek.MONDAY -> 1
|
||||
DayOfWeek.TUESDAY -> 1 shl 1
|
||||
DayOfWeek.WEDNESDAY -> 1 shl 2
|
||||
DayOfWeek.THURSDAY -> 1 shl 3
|
||||
DayOfWeek.FRIDAY -> 1 shl 4
|
||||
DayOfWeek.SATURDAY -> 1 shl 5
|
||||
DayOfWeek.SUNDAY -> 1 shl 6
|
||||
}
|
||||
}
|
||||
}
|
||||
fun asModel() = Service(
|
||||
id,
|
||||
Parser.deserialiseDays(days),
|
||||
LocalDate.fromEpochDays(start),
|
||||
LocalDate.fromEpochDays(end),
|
||||
)
|
||||
}
|
||||
|
||||
fun Service.asEntity() = ServiceEntity(
|
||||
id,
|
||||
ServiceEntity.Parser.serialiseDays(days),
|
||||
start.toEpochDays().toInt(),
|
||||
end.toEpochDays().toInt(),
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package moe.lava.banksia.room.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import moe.lava.banksia.model.Shape
|
||||
import moe.lava.banksia.model.ShapePath
|
||||
import moe.lava.banksia.room.converter.ShapePathConverter
|
||||
|
||||
@Entity("Shape")
|
||||
@TypeConverters(ShapePathConverter::class)
|
||||
data class ShapeEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val path: ShapePath,
|
||||
) {
|
||||
fun asModel() = Shape(id, path)
|
||||
}
|
||||
|
||||
fun Shape.asEntity() = ShapeEntity(id, path)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package moe.lava.banksia.room.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import moe.lava.banksia.model.Stop
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
@Entity("Stop")
|
||||
data class StopEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val name: String,
|
||||
val lat: Double,
|
||||
val lng: Double,
|
||||
@ColumnInfo(index = true) val parent: String,
|
||||
val hasWheelChairBoarding: Boolean,
|
||||
val level: String,
|
||||
val platformCode: String,
|
||||
) {
|
||||
fun asModel() = Stop(id, name, Point(lat, lng), parent, hasWheelChairBoarding, level, platformCode)
|
||||
}
|
||||
|
||||
fun Stop.asEntity() = StopEntity(id, name, pos.lat, pos.lng, parent, hasWheelChairBoarding, level, platformCode)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package moe.lava.banksia.room.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import moe.lava.banksia.model.FutureTime
|
||||
import moe.lava.banksia.model.FutureTime.Companion.asInt
|
||||
import moe.lava.banksia.model.StopTime
|
||||
|
||||
@Entity(
|
||||
"StopTime",
|
||||
primaryKeys = ["tripId", "stopId"],
|
||||
foreignKeys = [
|
||||
ForeignKey(TripEntity::class, parentColumns = ["id"], childColumns = ["tripId"], onDelete = CASCADE),
|
||||
ForeignKey(StopEntity::class, parentColumns = ["id"], childColumns = ["stopId"], onDelete = CASCADE),
|
||||
]
|
||||
)
|
||||
data class StopTimeEntity(
|
||||
val tripId: String,
|
||||
val stopId: String,
|
||||
val arrivalTime: Int,
|
||||
val departureTime: Int,
|
||||
val headsign: String?,
|
||||
val pickupType: Int,
|
||||
val dropOffType: Int,
|
||||
) {
|
||||
fun asModel() = StopTime(
|
||||
tripId,
|
||||
stopId,
|
||||
FutureTime.fromInt(arrivalTime),
|
||||
FutureTime.fromInt(departureTime),
|
||||
headsign,
|
||||
pickupType,
|
||||
dropOffType,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun StopTime.asEntity() = StopTimeEntity(
|
||||
tripId,
|
||||
stopId,
|
||||
arrivalTime.asInt(),
|
||||
departureTime.asInt(),
|
||||
headsign,
|
||||
pickupType,
|
||||
dropOffType,
|
||||
)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package moe.lava.banksia.room.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.PrimaryKey
|
||||
import moe.lava.banksia.model.Trip
|
||||
|
||||
@Entity(
|
||||
"Trip",
|
||||
foreignKeys = [
|
||||
ForeignKey(RouteEntity::class, parentColumns = ["id"], childColumns = ["routeId"], onDelete = CASCADE),
|
||||
ForeignKey(ShapeEntity::class, parentColumns = ["id"], childColumns = ["shapeId"], onDelete = CASCADE),
|
||||
],
|
||||
)
|
||||
data class TripEntity(
|
||||
@PrimaryKey val id: String,
|
||||
@ColumnInfo(index = true) val routeId: String,
|
||||
val serviceId: String,
|
||||
val shapeId: String?,
|
||||
val tripHeadsign: String,
|
||||
val directionId: String,
|
||||
val blockId: String,
|
||||
val wheelchairAccessible: String,
|
||||
) {
|
||||
fun asModel() = Trip(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible)
|
||||
}
|
||||
|
||||
fun Trip.asEntity() = TripEntity(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible)
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
package moe.lava.banksia.util
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Point(val lat: Double, val lng: Double)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.room.RoomDatabase
|
|||
import moe.lava.banksia.room.Database
|
||||
import org.koin.core.parameter.ParametersHolder
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
|
||||
class IosDatabaseBuilder() : PlatformDatabaseBuilder {
|
||||
override fun getBuilder(): RoomDatabase.Builder<Database> {
|
||||
|
|
@ -13,3 +14,5 @@ class IosDatabaseBuilder() : PlatformDatabaseBuilder {
|
|||
|
||||
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
|
||||
IosDatabaseBuilder()
|
||||
|
||||
internal actual val ExtPlatformModule = module { }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.room.RoomDatabase
|
|||
import moe.lava.banksia.room.Database
|
||||
import org.koin.core.parameter.ParametersHolder
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
import java.io.File
|
||||
|
||||
class JvmDatabaseBuilder() : PlatformDatabaseBuilder {
|
||||
|
|
@ -18,3 +19,5 @@ class JvmDatabaseBuilder() : PlatformDatabaseBuilder {
|
|||
|
||||
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
|
||||
JvmDatabaseBuilder()
|
||||
|
||||
internal actual val ExtPlatformModule = module { }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue