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.location)
|
||||||
implementation(libs.play.services.maps)
|
implementation(libs.play.services.maps)
|
||||||
implementation(libs.maps.compose)
|
implementation(libs.maps.compose)
|
||||||
|
implementation(libs.maps.compose.utils)
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
|
|
@ -53,8 +54,13 @@ kotlin {
|
||||||
implementation(libs.androidx.lifecycle.viewmodel)
|
implementation(libs.androidx.lifecycle.viewmodel)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.viewmodel)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.datetime)
|
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)
|
||||||
implementation(libs.moko.geo.compose)
|
implementation(libs.moko.geo.compose)
|
||||||
implementation(projects.shared)
|
implementation(projects.shared)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.geo.API_KEY"
|
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 com.google.maps.android.compose.rememberUpdatedMarkerState
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import moe.lava.banksia.R
|
import moe.lava.banksia.R
|
||||||
import moe.lava.banksia.ui.BanksiaEvent
|
|
||||||
import moe.lava.banksia.ui.components.RouteIcon
|
import moe.lava.banksia.ui.components.RouteIcon
|
||||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
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.ui.state.MapState
|
||||||
import moe.lava.banksia.util.BoxedValue
|
import moe.lava.banksia.util.BoxedValue
|
||||||
import moe.lava.banksia.util.Point
|
import moe.lava.banksia.util.Point
|
||||||
|
|
@ -67,7 +67,7 @@ actual fun getScreenHeight(): Int {
|
||||||
actual fun Maps(
|
actual fun Maps(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
state: MapState,
|
state: MapState,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||||
setLastKnownLocation: (Point) -> Unit,
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
extInsets: WindowInsets,
|
extInsets: WindowInsets,
|
||||||
|
|
@ -135,7 +135,7 @@ actual fun Maps(
|
||||||
zIndex = 0f,
|
zIndex = 0f,
|
||||||
state = state,
|
state = state,
|
||||||
onClick = {
|
onClick = {
|
||||||
onEvent(BanksiaEvent.SelectStop(marker.type to marker.id))
|
onEvent(MapScreenEvent.SelectStop(marker.type to marker.id))
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -155,7 +155,7 @@ actual fun Maps(
|
||||||
zIndex = 1f,
|
zIndex = 1f,
|
||||||
state = state,
|
state = state,
|
||||||
onClick = {
|
onClick = {
|
||||||
onEvent(BanksiaEvent.SelectRun(marker.ref))
|
onEvent(MapScreenEvent.SelectRun(marker.ref))
|
||||||
false
|
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
|
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.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.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.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import moe.lava.banksia.client.di.ClientModule
|
||||||
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
import moe.lava.banksia.di.CommonModules
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import moe.lava.banksia.ui.screens.MapScreen
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import org.koin.compose.KoinMultiplatformApplication
|
||||||
import androidx.compose.ui.unit.dp
|
import org.koin.core.annotation.KoinExperimentalAPI
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import org.koin.dsl.koinConfiguration
|
||||||
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
|
|
||||||
|
|
||||||
val MELBOURNE = Point(-37.8136, 144.9631)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, KoinExperimentalAPI::class)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App(
|
fun App() {
|
||||||
viewModel: BanksiaViewModel = viewModel()
|
KoinMultiplatformApplication(config = koinConfiguration {
|
||||||
) {
|
modules(CommonModules, ClientModule)
|
||||||
val scope = rememberCoroutineScope()
|
}) {
|
||||||
|
MapScreen()
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,15 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||||
|
import moe.lava.banksia.model.RouteType
|
||||||
|
import moe.lava.banksia.model.RouteType.Interstate
|
||||||
|
import moe.lava.banksia.model.RouteType.MetroBus
|
||||||
|
import moe.lava.banksia.model.RouteType.MetroTrain
|
||||||
|
import moe.lava.banksia.model.RouteType.MetroTram
|
||||||
|
import moe.lava.banksia.model.RouteType.RegionalBus
|
||||||
|
import moe.lava.banksia.model.RouteType.RegionalCoach
|
||||||
|
import moe.lava.banksia.model.RouteType.RegionalTrain
|
||||||
|
import moe.lava.banksia.model.RouteType.SkyBus
|
||||||
import moe.lava.banksia.resources.Res
|
import moe.lava.banksia.resources.Res
|
||||||
import moe.lava.banksia.resources.bus
|
import moe.lava.banksia.resources.bus
|
||||||
import moe.lava.banksia.resources.bus_background
|
import moe.lava.banksia.resources.bus_background
|
||||||
|
|
@ -33,12 +42,51 @@ data class RouteTypeProperties(
|
||||||
val icon: DrawableResource,
|
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 {
|
fun PtvRouteType.getUIProperties(): RouteTypeProperties {
|
||||||
val colour = when (this) {
|
val colour = when (this) {
|
||||||
PtvRouteType.TRAIN -> Color(0xFF0072CE)
|
PtvRouteType.TRAIN -> Color(TRAIN_BLUE)
|
||||||
PtvRouteType.TRAM -> Color(0xFF78BE20)
|
PtvRouteType.TRAM -> Color(TRAM_GREEN)
|
||||||
PtvRouteType.BUS, PtvRouteType.NIGHT_BUS -> Color(0xFFFF8200)
|
PtvRouteType.BUS, PtvRouteType.NIGHT_BUS -> Color(BUS_ORANGE)
|
||||||
PtvRouteType.VLINE -> Color(0xFF8F1A95)
|
PtvRouteType.VLINE -> Color(VLINE_PURPLE)
|
||||||
}
|
}
|
||||||
val (drawable, background, icon) = when (this) {
|
val (drawable, background, icon) = when (this) {
|
||||||
PtvRouteType.TRAM -> Triple(
|
PtvRouteType.TRAM -> Triple(
|
||||||
|
|
@ -58,7 +106,7 @@ fun PtvRouteType.getUIProperties(): RouteTypeProperties {
|
||||||
fun RouteIcon(
|
fun RouteIcon(
|
||||||
modifier: Modifier = Modifier.Companion,
|
modifier: Modifier = Modifier.Companion,
|
||||||
size: Dp = 40.dp,
|
size: Dp = 40.dp,
|
||||||
routeType: PtvRouteType,
|
routeType: RouteType,
|
||||||
) {
|
) {
|
||||||
val properties = routeType.getUIProperties()
|
val properties = routeType.getUIProperties()
|
||||||
Image(
|
Image(
|
||||||
|
|
@ -80,9 +128,9 @@ const val ICON_PADDING = 0.25f
|
||||||
@Composable
|
@Composable
|
||||||
private fun RouteIconPreview() {
|
private fun RouteIconPreview() {
|
||||||
Row {
|
Row {
|
||||||
RouteIcon(routeType = PtvRouteType.TRAIN)
|
RouteIcon(routeType = RouteType.MetroTrain)
|
||||||
RouteIcon(routeType = PtvRouteType.TRAM)
|
RouteIcon(routeType = RouteType.MetroTram)
|
||||||
RouteIcon(routeType = PtvRouteType.BUS)
|
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.Dp
|
||||||
import androidx.compose.ui.unit.coerceAtMost
|
import androidx.compose.ui.unit.coerceAtMost
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import moe.lava.banksia.ui.BanksiaEvent
|
|
||||||
import moe.lava.banksia.ui.components.RouteIcon
|
import moe.lava.banksia.ui.components.RouteIcon
|
||||||
|
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||||
import moe.lava.banksia.ui.state.InfoPanelState
|
import moe.lava.banksia.ui.state.InfoPanelState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InfoPanel(
|
fun InfoPanel(
|
||||||
state: InfoPanelState,
|
state: InfoPanelState,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
onPeekHeightChange: (Dp) -> Unit,
|
onPeekHeightChange: (Dp) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (state is InfoPanelState.None)
|
if (state is InfoPanelState.None)
|
||||||
|
|
@ -74,7 +74,7 @@ fun InfoPanel(
|
||||||
@Composable
|
@Composable
|
||||||
private inline fun RouteInfoPanel(
|
private inline fun RouteInfoPanel(
|
||||||
state: InfoPanelState.Route,
|
state: InfoPanelState.Route,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.Companion.fillMaxWidth()) {
|
Column(Modifier.Companion.fillMaxWidth()) {
|
||||||
Row {
|
Row {
|
||||||
|
|
@ -92,7 +92,7 @@ private inline fun RouteInfoPanel(
|
||||||
@Composable
|
@Composable
|
||||||
private inline fun RunInfoPanel(
|
private inline fun RunInfoPanel(
|
||||||
state: InfoPanelState.Run,
|
state: InfoPanelState.Run,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.Companion.fillMaxWidth()) {
|
Column(Modifier.Companion.fillMaxWidth()) {
|
||||||
Row {
|
Row {
|
||||||
|
|
@ -110,7 +110,7 @@ private inline fun RunInfoPanel(
|
||||||
@Composable
|
@Composable
|
||||||
private inline fun StopInfoPanel(
|
private inline fun StopInfoPanel(
|
||||||
state: InfoPanelState.Stop,
|
state: InfoPanelState.Stop,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.Companion.fillMaxWidth()) {
|
Column(Modifier.Companion.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,15 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import moe.lava.banksia.ui.BanksiaEvent
|
|
||||||
import moe.lava.banksia.ui.components.RouteIcon
|
import moe.lava.banksia.ui.components.RouteIcon
|
||||||
|
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||||
import moe.lava.banksia.ui.state.SearchState
|
import moe.lava.banksia.ui.state.SearchState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Searcher(
|
fun Searcher(
|
||||||
state: SearchState,
|
state: SearchState,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onExpandedChange: (Boolean) -> Unit,
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -55,7 +55,7 @@ fun Searcher(
|
||||||
SearchBarDefaults.InputField(
|
SearchBarDefaults.InputField(
|
||||||
modifier = Modifier.Companion.padding(horizontal = 20.dp - animatedPadding),
|
modifier = Modifier.Companion.padding(horizontal = 20.dp - animatedPadding),
|
||||||
query = state.text,
|
query = state.text,
|
||||||
onQueryChange = { onEvent(BanksiaEvent.SearchUpdate(it)) },
|
onQueryChange = { onEvent(MapScreenEvent.SearchUpdate(it)) },
|
||||||
onSearch = {},
|
onSearch = {},
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onExpandedChange = onExpandedChange,
|
onExpandedChange = onExpandedChange,
|
||||||
|
|
@ -67,7 +67,7 @@ fun Searcher(
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.Companion.clickable {
|
modifier = Modifier.Companion.clickable {
|
||||||
onEvent(
|
onEvent(
|
||||||
BanksiaEvent.SearchUpdate(
|
MapScreenEvent.SearchUpdate(
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -92,8 +92,8 @@ fun Searcher(
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
onExpandedChange(false)
|
onExpandedChange(false)
|
||||||
onEvent(BanksiaEvent.SearchUpdate(""))
|
onEvent(MapScreenEvent.SearchUpdate(""))
|
||||||
onEvent(BanksiaEvent.SelectRoute(entry.routeId))
|
onEvent(MapScreenEvent.SelectRoute(entry.routeId))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import kotlinx.coroutines.flow.Flow
|
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.ui.state.MapState
|
||||||
import moe.lava.banksia.util.BoxedValue
|
import moe.lava.banksia.util.BoxedValue
|
||||||
import moe.lava.banksia.util.Point
|
import moe.lava.banksia.util.Point
|
||||||
|
|
@ -18,7 +18,7 @@ expect fun getScreenHeight(): Int
|
||||||
expect fun Maps(
|
expect fun Maps(
|
||||||
modifier: Modifier = Modifier.Companion,
|
modifier: Modifier = Modifier.Companion,
|
||||||
state: MapState,
|
state: MapState,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||||
setLastKnownLocation: (Point) -> Unit,
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
extInsets: WindowInsets,
|
extInsets: WindowInsets,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package moe.lava.banksia.ui.platform.maps
|
package moe.lava.banksia.ui.platform.maps
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
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
|
import moe.lava.banksia.util.Point
|
||||||
|
|
||||||
sealed class Marker {
|
sealed class Marker {
|
||||||
|
|
@ -9,14 +9,14 @@ sealed class Marker {
|
||||||
|
|
||||||
data class Stop(
|
data class Stop(
|
||||||
override val point: Point,
|
override val point: Point,
|
||||||
val id: Int,
|
val id: String,
|
||||||
val type: PtvRouteType,
|
val type: RouteType,
|
||||||
val colour: Color,
|
val colour: Color,
|
||||||
) : Marker()
|
) : Marker()
|
||||||
|
|
||||||
data class Vehicle(
|
data class Vehicle(
|
||||||
override val point: Point,
|
override val point: Point,
|
||||||
val ref: String,
|
val ref: String,
|
||||||
val type: PtvRouteType,
|
val type: RouteType,
|
||||||
) : Marker()
|
) : 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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -13,9 +13,12 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import 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.PtvService
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
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.components.getUIProperties
|
||||||
import moe.lava.banksia.ui.platform.maps.CameraPosition
|
import moe.lava.banksia.ui.platform.maps.CameraPosition
|
||||||
import moe.lava.banksia.ui.platform.maps.CameraPositionBounds
|
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.Clock
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
sealed class BanksiaEvent {
|
sealed class MapScreenEvent {
|
||||||
data object DismissState : BanksiaEvent()
|
data object DismissState : MapScreenEvent()
|
||||||
|
|
||||||
data class SelectRoute(val id: Int?) : BanksiaEvent()
|
data class SelectRoute(val id: String?) : MapScreenEvent()
|
||||||
data class SelectRun(val ref: String?) : BanksiaEvent()
|
data class SelectRun(val ref: String?) : MapScreenEvent()
|
||||||
data class SelectStop(val typeAndId: Pair<PtvRouteType, Int>) : BanksiaEvent()
|
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(
|
data class InternalState(
|
||||||
val route: Int? = null,
|
val route: String? = null,
|
||||||
val stop: Pair<PtvRouteType, Int>? = null,
|
val stop: Pair<RouteType, String>? = null,
|
||||||
val run: 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()
|
private var state = InternalState()
|
||||||
set(value) {
|
set(value) {
|
||||||
val last = field
|
val last = field
|
||||||
|
|
@ -72,7 +79,6 @@ class BanksiaViewModel : ViewModel() {
|
||||||
private val iSearchState = MutableStateFlow(SearchState())
|
private val iSearchState = MutableStateFlow(SearchState())
|
||||||
val searchState = iSearchState.asStateFlow()
|
val searchState = iSearchState.asStateFlow()
|
||||||
|
|
||||||
private val ptvService = PtvService(viewModelScope)
|
|
||||||
private var locationTrackerJob: Job? = null
|
private var locationTrackerJob: Job? = null
|
||||||
private var lastKnownLocation: Point? = null
|
private var lastKnownLocation: Point? = null
|
||||||
|
|
||||||
|
|
@ -80,14 +86,14 @@ class BanksiaViewModel : ViewModel() {
|
||||||
viewModelScope.launch { searchUpdate("") }
|
viewModelScope.launch { searchUpdate("") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: BanksiaEvent) {
|
fun handleEvent(event: MapScreenEvent) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (event) {
|
when (event) {
|
||||||
is BanksiaEvent.DismissState -> dismissState()
|
is MapScreenEvent.DismissState -> dismissState()
|
||||||
is BanksiaEvent.SelectRoute -> state = InternalState(route = event.id)
|
is MapScreenEvent.SelectRoute -> state = InternalState(route = event.id)
|
||||||
is BanksiaEvent.SelectRun -> state = state.copy(run = event.ref, stop = null)
|
is MapScreenEvent.SelectRun -> state = state.copy(run = event.ref, stop = null)
|
||||||
is BanksiaEvent.SelectStop -> state = state.copy(stop = event.typeAndId, run = null)
|
is MapScreenEvent.SelectStop -> state = state.copy(stop = event.typeIdPair, run = null)
|
||||||
is BanksiaEvent.SearchUpdate -> searchUpdate(event.text)
|
is MapScreenEvent.SearchUpdate -> searchUpdate(event.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +105,11 @@ class BanksiaViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun centreCameraToLocation() {
|
fun centreCameraToLocation() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
log("msvm", "getting..")
|
||||||
|
val routes = routeRepository.getAll()
|
||||||
|
log("msvm", routes.joinToString("\n"))
|
||||||
|
}
|
||||||
lastKnownLocation?.let { location ->
|
lastKnownLocation?.let { location ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
log("bvm", "emitting $location")
|
log("bvm", "emitting $location")
|
||||||
|
|
@ -117,46 +128,48 @@ class BanksiaViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun searchUpdate(text: String) {
|
private suspend fun searchUpdate(text: String) {
|
||||||
val entries = ptvService.routes()
|
iSearchState.update { it.copy(text = text) }
|
||||||
|
val entries = routeRepository.getAll()
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
{ it.gtfsSubType()?.ordinal },
|
{ it.type.ordinal },
|
||||||
{ it.routeNumber.toIntOrNull() },
|
{ it.number },
|
||||||
{ it.routeName }
|
{ 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 ->
|
.map { route ->
|
||||||
val (main, sub) = if (route.routeNumber.isNotEmpty()) {
|
val (main, sub) = if (route.number?.isNotEmpty() == true) {
|
||||||
route.routeNumber to route.routeName
|
route.number to route.name
|
||||||
} else {
|
} 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) }
|
iSearchState.update { SearchState(entries, text) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun switchRoute(routeId: Int?) {
|
private suspend fun switchRoute(routeId: String?) {
|
||||||
iMapState.update { MapState() }
|
iMapState.update { MapState() }
|
||||||
if (routeId == null) {
|
if (routeId == null) {
|
||||||
iInfoState.update { InfoPanelState.None }
|
iInfoState.update { InfoPanelState.None }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val route = ptvService.route(routeId)
|
val route = routeRepository.get(routeId)
|
||||||
|
// val gtfsRoute = ptvService.route(routeId)
|
||||||
iInfoState.update {
|
iInfoState.update {
|
||||||
InfoPanelState.Route(
|
InfoPanelState.Route(
|
||||||
name = route.routeName,
|
name = route.name,
|
||||||
type = route.routeType,
|
type = route.type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch { buildPolylines(route) }
|
// viewModelScope.launch { buildPolylines(gtfsRoute) }
|
||||||
viewModelScope.launch { buildStops(route) }
|
viewModelScope.launch { buildStops(route) }
|
||||||
buildRuns(route)
|
// buildRuns(gtfsRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchRun(ref: String?) {
|
private fun switchRun(ref: String?) {
|
||||||
|
|
@ -175,7 +188,7 @@ class BanksiaViewModel : ViewModel() {
|
||||||
iInfoState.update {
|
iInfoState.update {
|
||||||
InfoPanelState.Run(
|
InfoPanelState.Run(
|
||||||
direction = run.destinationName,
|
direction = run.destinationName,
|
||||||
type = run.routeType,
|
type = RouteType.MetroTrain, // XXX HACK TODO FIXME
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
routeName = ptvService.route(run.routeId).routeName
|
routeName = ptvService.route(run.routeId).routeName
|
||||||
|
|
@ -184,7 +197,7 @@ class BanksiaViewModel : ViewModel() {
|
||||||
iInfoState.update {
|
iInfoState.update {
|
||||||
InfoPanelState.Run(
|
InfoPanelState.Run(
|
||||||
direction = run.destinationName,
|
direction = run.destinationName,
|
||||||
type = run.routeType,
|
type = RouteType.MetroTrain, // FIXME HACK XXX TODO
|
||||||
routeName = routeName,
|
routeName = routeName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -193,25 +206,27 @@ class BanksiaViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// [TODO]: Cleanup
|
// [TODO]: Cleanup
|
||||||
private suspend fun switchStop(typeAndId: Pair<PtvRouteType, Int>?) {
|
private suspend fun switchStop(pair: Pair<RouteType, String>?) {
|
||||||
if (typeAndId == null) {
|
if (pair == null) {
|
||||||
iInfoState.update { InfoPanelState.None }
|
iInfoState.update { InfoPanelState.None }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val (routeType, stopId) = typeAndId
|
val (type, id) = pair
|
||||||
val stop = ptvService.stop(routeType, stopId)
|
|
||||||
val split = stop.stopName.split("/")
|
val stop = stopRepository.get(id)
|
||||||
|
// val stop = ptvService.stop(routeType, stopId)
|
||||||
|
val split = stop.name.split("/")
|
||||||
val name = split[0]
|
val name = split[0]
|
||||||
val subname = split.getOrNull(1)
|
val subname = split.getOrNull(1)
|
||||||
iInfoState.update {
|
iInfoState.update {
|
||||||
InfoPanelState.Stop(
|
InfoPanelState.Stop(
|
||||||
id = stop.stopId,
|
id = stop.id,
|
||||||
name = name,
|
name = name,
|
||||||
subname = subname,
|
subname = subname,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val res = ptvService.departures(stop.routeType, stop.stopId)
|
val res = ptvService.departures(type, stop.id)
|
||||||
// Map<
|
// Map<
|
||||||
// Pair<DirectionId, RouteId>,
|
// Pair<DirectionId, RouteId>,
|
||||||
// Pair<DirectionName, List<DepartureTimes>>
|
// Pair<DirectionName, List<DepartureTimes>>
|
||||||
|
|
@ -285,7 +300,7 @@ class BanksiaViewModel : ViewModel() {
|
||||||
ptvService
|
ptvService
|
||||||
.runsFlow(route.routeId)
|
.runsFlow(route.routeId)
|
||||||
.waitUntilSubscribed(iInfoState)
|
.waitUntilSubscribed(iInfoState)
|
||||||
.takeWhile { state.route == route.routeId }
|
// .takeWhile { state.route == route.routeId }
|
||||||
.onEach { runs ->
|
.onEach { runs ->
|
||||||
val markers = runs
|
val markers = runs
|
||||||
.filter { it.vehiclePosition != null }
|
.filter { it.vehiclePosition != null }
|
||||||
|
|
@ -295,7 +310,7 @@ class BanksiaViewModel : ViewModel() {
|
||||||
Marker.Vehicle(
|
Marker.Vehicle(
|
||||||
Point(pos.latitude, pos.longitude),
|
Point(pos.latitude, pos.longitude),
|
||||||
ref = run.runRef,
|
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) {
|
private suspend fun buildStops(route: Route) {
|
||||||
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
val stops = stopRepository.getByRoute(route.id)
|
||||||
val colour = route.routeType.getUIProperties().colour
|
val colour = route.type.getUIProperties().colour
|
||||||
|
|
||||||
val markers = stops
|
val markers = stops
|
||||||
.filter { it.stopLatitude != null && it.stopLongitude != null }
|
|
||||||
.map { stop ->
|
.map { stop ->
|
||||||
Marker.Stop(
|
Marker.Stop(
|
||||||
point = Point(stop.stopLatitude!!, stop.stopLongitude!!),
|
point = stop.pos,
|
||||||
id = stop.stopId,
|
id = stop.id,
|
||||||
colour = colour,
|
colour = colour,
|
||||||
type = route.routeType,
|
type = route.type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package moe.lava.banksia.ui.state
|
package moe.lava.banksia.ui.state
|
||||||
|
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
import moe.lava.banksia.model.RouteType
|
||||||
|
|
||||||
sealed class InfoPanelState {
|
sealed class InfoPanelState {
|
||||||
abstract val loading: Boolean
|
abstract val loading: Boolean
|
||||||
|
|
@ -11,21 +11,21 @@ sealed class InfoPanelState {
|
||||||
|
|
||||||
data class Route(
|
data class Route(
|
||||||
val name: String,
|
val name: String,
|
||||||
val type: PtvRouteType,
|
val type: RouteType,
|
||||||
) : InfoPanelState() {
|
) : InfoPanelState() {
|
||||||
override val loading = false
|
override val loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Run(
|
data class Run(
|
||||||
val direction: String,
|
val direction: String,
|
||||||
val type: PtvRouteType,
|
val type: RouteType,
|
||||||
val routeName: String? = null,
|
val routeName: String? = null,
|
||||||
) : InfoPanelState() {
|
) : InfoPanelState() {
|
||||||
override val loading = routeName == null
|
override val loading = routeName == null
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Stop(
|
data class Stop(
|
||||||
val id: Int,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val subname: String? = null,
|
val subname: String? = null,
|
||||||
val departures: List<Departure>? = null,
|
val departures: List<Departure>? = null,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package moe.lava.banksia.ui.state
|
package moe.lava.banksia.ui.state
|
||||||
|
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
import moe.lava.banksia.model.RouteType
|
||||||
|
|
||||||
data class SearchState(
|
data class SearchState(
|
||||||
val entries: List<SearchEntry> = listOf(),
|
val entries: List<SearchEntry> = listOf(),
|
||||||
|
|
@ -9,7 +9,7 @@ data class SearchState(
|
||||||
data class SearchEntry(
|
data class SearchEntry(
|
||||||
val mainText: String,
|
val mainText: String,
|
||||||
val subText: String?,
|
val subText: String?,
|
||||||
val routeId: Int,
|
val routeId: String,
|
||||||
val routeType: PtvRouteType,
|
val routeType: RouteType,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
import kotlinx.coroutines.flow.Flow
|
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.ui.state.MapState
|
||||||
import moe.lava.banksia.util.BoxedValue
|
import moe.lava.banksia.util.BoxedValue
|
||||||
import moe.lava.banksia.util.Point
|
import moe.lava.banksia.util.Point
|
||||||
|
|
@ -23,7 +23,7 @@ actual fun getScreenHeight(): Int {
|
||||||
actual fun Maps(
|
actual fun Maps(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
state: MapState,
|
state: MapState,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (MapScreenEvent) -> Unit,
|
||||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||||
setLastKnownLocation: (Point) -> Unit,
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
extInsets: WindowInsets,
|
extInsets: WindowInsets,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ koin = "4.1.0"
|
||||||
kotlin = "2.2.0"
|
kotlin = "2.2.0"
|
||||||
kotlinxDatetime = "0.7.1"
|
kotlinxDatetime = "0.7.1"
|
||||||
kotlinxSerializationCsv = "0.2.18"
|
kotlinxSerializationCsv = "0.2.18"
|
||||||
kotlinxSerializationJson = "1.9.0"
|
kotlinxSerialization = "1.9.0"
|
||||||
ksp = "2.2.0-2.0.2"
|
ksp = "2.2.0-2.0.2"
|
||||||
ktor = "3.2.3"
|
ktor = "3.2.3"
|
||||||
logback = "1.5.18"
|
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 = { 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-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" }
|
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-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||||
koin-ktor = { module = "io.insert-koin:koin-ktor", 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-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-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
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-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-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-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", 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-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-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-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-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
|
||||||
ktor-server-tests = { module = "io.ktor:ktor-server-test-host", 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" }
|
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
|
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" }
|
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-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" }
|
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.core)
|
||||||
implementation(libs.koin.ktor)
|
implementation(libs.koin.ktor)
|
||||||
implementation(libs.kotlinx.serialization.csv)
|
implementation(libs.kotlinx.serialization.csv)
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.ktor.client.contentnegotiation)
|
implementation(libs.ktor.client.contentnegotiation)
|
||||||
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.okhttp)
|
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.core)
|
||||||
implementation(libs.ktor.server.netty)
|
implementation(libs.ktor.server.netty)
|
||||||
implementation(libs.room.runtime)
|
implementation(libs.room.runtime)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
package moe.lava.banksia.server
|
package moe.lava.banksia.server
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
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.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.application.log
|
import io.ktor.server.application.log
|
||||||
import io.ktor.server.engine.embeddedServer
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.server.netty.Netty
|
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.response.respondText
|
||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import moe.lava.banksia.di.CommonModules
|
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.di.ServerModules
|
||||||
import moe.lava.banksia.server.gtfs.GtfsHandler
|
import moe.lava.banksia.server.gtfs.GtfsHandler
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
@ -24,6 +31,9 @@ fun main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
install(Koin) {
|
install(Koin) {
|
||||||
modules(module { single { log } })
|
modules(module { single { log } })
|
||||||
modules(CommonModules, ServerModules)
|
modules(CommonModules, ServerModules)
|
||||||
|
|
@ -40,5 +50,66 @@ fun Application.module() {
|
||||||
handler.update(datasetUrl)
|
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.decodeFromString
|
||||||
import kotlinx.serialization.modules.EmptySerializersModule
|
import kotlinx.serialization.modules.EmptySerializersModule
|
||||||
import moe.lava.banksia.model.Route
|
import moe.lava.banksia.model.Route
|
||||||
import moe.lava.banksia.model.RouteType
|
|
||||||
import moe.lava.banksia.model.Shape
|
import moe.lava.banksia.model.Shape
|
||||||
import moe.lava.banksia.room.dao.RouteDao
|
import moe.lava.banksia.model.Stop
|
||||||
import moe.lava.banksia.room.dao.ShapeDao
|
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.GtfsRoute
|
||||||
import moe.lava.banksia.server.gtfs.structures.GtfsShape
|
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 moe.lava.banksia.util.Point
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
@ -25,9 +31,7 @@ import java.util.zip.ZipFile
|
||||||
class GtfsHandler(
|
class GtfsHandler(
|
||||||
private val log: Logger,
|
private val log: Logger,
|
||||||
private val client: HttpClient,
|
private val client: HttpClient,
|
||||||
|
private val db: Database,
|
||||||
private val routeDao: RouteDao,
|
|
||||||
private val shapeDao: ShapeDao,
|
|
||||||
) {
|
) {
|
||||||
private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule()))
|
private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule()))
|
||||||
private val datasetPath = File("/tmp/banksia", "dataset.zip")
|
private val datasetPath = File("/tmp/banksia", "dataset.zip")
|
||||||
|
|
@ -49,27 +53,30 @@ class GtfsHandler(
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("extracting...")
|
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...")
|
log.info("parsing routes...")
|
||||||
val routes = files
|
val routes = files
|
||||||
.filter { it.name == "routes.txt" }
|
.filter { it.name == "routes.txt" }
|
||||||
.flatMap { fd -> parseRoutes(fd) }
|
.flatMap { fd -> parseRoutes(fd) }
|
||||||
|
|
||||||
log.info("inserting routes...")
|
log.info("inserting routes...")
|
||||||
routeDao.deleteAll()
|
dao.deleteAll()
|
||||||
routeDao.insertAll(*routes.toTypedArray())
|
dao.insertAll(*routes.map { it.asEntity() }.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!")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseRoutes(fd: File) =
|
private fun parseRoutes(fd: File) =
|
||||||
|
|
@ -77,12 +84,24 @@ class GtfsHandler(
|
||||||
.map { with(it) {
|
.map { with(it) {
|
||||||
Route(
|
Route(
|
||||||
id = route_id,
|
id = route_id,
|
||||||
type = RouteType.from(fd.parentFile.name.toInt()),
|
type = RouteTypeConverter.from(fd.parentFile.name.toInt()),
|
||||||
number = route_short_name,
|
number = route_short_name,
|
||||||
name = route_long_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) =
|
private fun parseShapes(fd: File) =
|
||||||
fd.parseCsv<GtfsShape>()
|
fd.parseCsv<GtfsShape>()
|
||||||
.groupBy { it.shape_id }
|
.groupBy { it.shape_id }
|
||||||
|
|
@ -94,6 +113,95 @@ class GtfsHandler(
|
||||||
Shape(id, points)
|
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> {
|
private fun extract(fd: File): List<File> {
|
||||||
val outputs = mutableListOf<File>()
|
val outputs = mutableListOf<File>()
|
||||||
|
|
@ -114,7 +222,7 @@ class GtfsHandler(
|
||||||
|
|
||||||
private fun extractAll(fd: File) = extract(fd).flatMap(::extract)
|
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()
|
.readText()
|
||||||
.replace("\uFEFF", "") // remove bom
|
.replace("\uFEFF", "") // remove bom
|
||||||
.replace("\r\n", "\n") // crlf -> lf
|
.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 {
|
sourceSets {
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation(libs.ktor.client.okhttp)
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
|
|
@ -48,6 +49,7 @@ kotlin {
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.kotlinx.serialization.protobuf)
|
||||||
implementation(libs.room.runtime)
|
implementation(libs.room.runtime)
|
||||||
implementation(libs.sqlite.bundled)
|
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 moe.lava.banksia.room.Database
|
||||||
import org.koin.core.parameter.ParametersHolder
|
import org.koin.core.parameter.ParametersHolder
|
||||||
import org.koin.core.scope.Scope
|
import org.koin.core.scope.Scope
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
|
class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
|
||||||
override fun getBuilder(): RoomDatabase.Builder<Database> {
|
override fun getBuilder(): RoomDatabase.Builder<Database> {
|
||||||
|
|
@ -19,4 +20,6 @@ class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): 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.client.statement.HttpResponse
|
||||||
import io.ktor.http.appendPathSegments
|
import io.ktor.http.appendPathSegments
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
@ -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.PtvDirection
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
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.PtvRun
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvStop
|
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.LoopFlow.Companion.initWith
|
||||||
import moe.lava.banksia.util.error
|
import moe.lava.banksia.util.error
|
||||||
import moe.lava.banksia.util.log
|
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]!!
|
return this[key]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
class PtvService(coroutineScope: CoroutineScope) {
|
class PtvService() {
|
||||||
class PtvCache(
|
class PtvCache(
|
||||||
coroutineScope: CoroutineScope,
|
val directions: MutableMap<Pair<Int, Int>, PtvDirection> = mutableMapOf(),
|
||||||
val directions: CacheMap<Pair<Int, Int>, PtvDirection> = CacheMap(coroutineScope),
|
val routes: MutableMap<Int, PtvRoute> = mutableMapOf(),
|
||||||
val routes: CacheMap<Int, PtvRoute> = CacheMap(coroutineScope),
|
val runs: MutableMap<String, PtvRun> = mutableMapOf(),
|
||||||
val runs: CacheMap<String, PtvRun> = CacheMap(coroutineScope),
|
val stops: MutableMap<Int, PtvStop> = mutableMapOf(),
|
||||||
val stops: CacheMap<Int, PtvStop> = CacheMap(coroutineScope),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val cache = PtvCache(coroutineScope)
|
val cache = PtvCache()
|
||||||
|
|
||||||
private val client = HttpClient() {
|
private val client = HttpClient() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
|
|
@ -227,6 +226,20 @@ class PtvService(coroutineScope: CoroutineScope) {
|
||||||
return cache.directions[directionId to routeId]!!
|
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 =
|
suspend fun departures(routeType: PtvRouteType, stopId: Int): Responses.PtvDeparturesResponse =
|
||||||
client
|
client
|
||||||
.safeGet ("departures") {
|
.safeGet ("departures") {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import moe.lava.banksia.model.RouteType
|
||||||
|
|
||||||
private object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> {
|
private object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> {
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
|
||||||
|
|
@ -30,4 +31,20 @@ enum class PtvRouteType {
|
||||||
BUS,
|
BUS,
|
||||||
VLINE,
|
VLINE,
|
||||||
NIGHT_BUS,
|
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)
|
includes(PlatformModule)
|
||||||
|
|
||||||
single { Database.build(get<PlatformDatabaseBuilder>().getBuilder()) }
|
single { Database.build(get<PlatformDatabaseBuilder>().getBuilder()) }
|
||||||
single { get<Database>().getRouteDao() }
|
single { get<Database>().routeDao }
|
||||||
single { get<Database>().getShapeDao() }
|
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 androidx.room.RoomDatabase
|
||||||
import moe.lava.banksia.room.Database
|
import moe.lava.banksia.room.Database
|
||||||
|
import org.koin.core.module.Module
|
||||||
import org.koin.core.parameter.ParametersHolder
|
import org.koin.core.parameter.ParametersHolder
|
||||||
import org.koin.core.scope.Scope
|
import org.koin.core.scope.Scope
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
@ -12,6 +13,9 @@ interface PlatformDatabaseBuilder {
|
||||||
|
|
||||||
expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder
|
expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder
|
||||||
|
|
||||||
|
internal expect val ExtPlatformModule: Module
|
||||||
|
|
||||||
internal val PlatformModule = module {
|
internal val PlatformModule = module {
|
||||||
|
includes(ExtPlatformModule)
|
||||||
single { provideDatabaseBuilder(it) }
|
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
|
package moe.lava.banksia.model
|
||||||
|
|
||||||
import androidx.room.Entity
|
import kotlinx.serialization.Serializable
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity
|
@Serializable
|
||||||
data class Route(
|
data class Route(
|
||||||
@PrimaryKey val id: String,
|
val id: String,
|
||||||
val type: RouteType,
|
val type: RouteType,
|
||||||
val number: String?,
|
val number: String?,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.model
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
enum class RouteType(val value: Int) {
|
enum class RouteType(val value: Int) {
|
||||||
MetroTrain(2),
|
MetroTrain(2),
|
||||||
MetroTram(3),
|
MetroTram(3),
|
||||||
|
|
@ -12,12 +13,4 @@ enum class RouteType(val value: Int) {
|
||||||
SkyBus(11),
|
SkyBus(11),
|
||||||
Interstate(10),
|
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
|
package moe.lava.banksia.model
|
||||||
|
|
||||||
import androidx.room.Entity
|
import kotlinx.serialization.Serializable
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import androidx.room.TypeConverters
|
|
||||||
import moe.lava.banksia.room.converter.ShapeConverter
|
|
||||||
import moe.lava.banksia.util.Point
|
import moe.lava.banksia.util.Point
|
||||||
|
|
||||||
typealias ShapePath = List<Point>
|
typealias ShapePath = List<Point>
|
||||||
|
|
||||||
@Entity
|
@Serializable
|
||||||
@TypeConverters(ShapeConverter::class)
|
|
||||||
data class Shape(
|
data class Shape(
|
||||||
@PrimaryKey val id: String,
|
val id: String,
|
||||||
val path: ShapePath,
|
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
|
package moe.lava.banksia.room
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
import moe.lava.banksia.model.Route
|
import moe.lava.banksia.room.converter.RouteTypeConverter
|
||||||
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.RouteDao
|
||||||
import moe.lava.banksia.room.dao.ShapeDao
|
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
|
import androidx.room.Database as DatabaseAnnotation
|
||||||
|
|
||||||
@DatabaseAnnotation(entities = [Route::class, Shape::class], version = 1)
|
@DatabaseAnnotation(
|
||||||
@TypeConverters(RouteType.Companion::class)
|
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 class Database : RoomDatabase() {
|
||||||
abstract fun getRouteDao(): RouteDao
|
abstract val routeDao: RouteDao
|
||||||
abstract fun getShapeDao(): ShapeDao
|
abstract val shapeDao: ShapeDao
|
||||||
|
abstract val stopDao: StopDao
|
||||||
|
abstract val stopTimeDao: StopTimeDao
|
||||||
|
abstract val tripDao: TripDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun build(base: Builder<Database>) =
|
fun build(base: Builder<Database>) =
|
||||||
base.fallbackToDestructiveMigrationOnDowngrade(true)
|
base.fallbackToDestructiveMigration(true)
|
||||||
.setDriver(BundledSQLiteDriver())
|
.setDriver(BundledSQLiteDriver())
|
||||||
.setQueryCoroutineContext(Dispatchers.IO)
|
.setQueryCoroutineContext(Dispatchers.IO)
|
||||||
|
// .fallbackToDestructiveMigration(true)
|
||||||
.build()
|
.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.model.ShapePath
|
||||||
import moe.lava.banksia.util.Point
|
import moe.lava.banksia.util.Point
|
||||||
|
|
||||||
object ShapeConverter {
|
object ShapePathConverter {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun from(value: ByteArray): ShapePath {
|
fun from(value: ByteArray): ShapePath {
|
||||||
return value
|
return value
|
||||||
|
|
@ -3,23 +3,47 @@ package moe.lava.banksia.room.dao
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||||
import androidx.room.Query
|
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
|
@Dao
|
||||||
interface RouteDao {
|
interface RouteDao {
|
||||||
@Query("SELECT * FROM Route")
|
@Query("SELECT * FROM Route")
|
||||||
suspend fun getAll(): List<Route>
|
suspend fun getAll(): List<RouteEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM Route WHERE id == :id")
|
@Query("SELECT * FROM Route WHERE id == :id")
|
||||||
suspend fun get(id: String): Route?
|
suspend fun get(id: String): RouteEntity?
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertAll(vararg routes: Route)
|
suspend fun insertAll(vararg routes: RouteEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = REPLACE)
|
||||||
|
suspend fun insertOrReplaceAll(vararg routes: RouteEntity)
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(route: Route)
|
suspend fun delete(route: RouteEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM Route")
|
@Query("DELETE FROM Route")
|
||||||
suspend fun deleteAll()
|
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.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import moe.lava.banksia.model.Shape
|
import moe.lava.banksia.room.entity.ShapeEntity
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ShapeDao {
|
interface ShapeDao {
|
||||||
@Query("SELECT * FROM Shape WHERE id == :id")
|
@Query("SELECT * FROM Shape WHERE id == :id")
|
||||||
suspend fun get(id: String): Shape?
|
suspend fun get(id: String): ShapeEntity?
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertAll(vararg shapes: Shape)
|
suspend fun insertAll(vararg shapes: ShapeEntity)
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(shape: Shape)
|
suspend fun delete(shape: ShapeEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM Shape")
|
@Query("DELETE FROM Shape")
|
||||||
suspend fun deleteAll()
|
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
|
package moe.lava.banksia.util
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class Point(val lat: Double, val lng: Double)
|
data class Point(val lat: Double, val lng: Double)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.room.RoomDatabase
|
||||||
import moe.lava.banksia.room.Database
|
import moe.lava.banksia.room.Database
|
||||||
import org.koin.core.parameter.ParametersHolder
|
import org.koin.core.parameter.ParametersHolder
|
||||||
import org.koin.core.scope.Scope
|
import org.koin.core.scope.Scope
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
class IosDatabaseBuilder() : PlatformDatabaseBuilder {
|
class IosDatabaseBuilder() : PlatformDatabaseBuilder {
|
||||||
override fun getBuilder(): RoomDatabase.Builder<Database> {
|
override fun getBuilder(): RoomDatabase.Builder<Database> {
|
||||||
|
|
@ -13,3 +14,5 @@ class IosDatabaseBuilder() : PlatformDatabaseBuilder {
|
||||||
|
|
||||||
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
|
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
|
||||||
IosDatabaseBuilder()
|
IosDatabaseBuilder()
|
||||||
|
|
||||||
|
internal actual val ExtPlatformModule = module { }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.room.RoomDatabase
|
||||||
import moe.lava.banksia.room.Database
|
import moe.lava.banksia.room.Database
|
||||||
import org.koin.core.parameter.ParametersHolder
|
import org.koin.core.parameter.ParametersHolder
|
||||||
import org.koin.core.scope.Scope
|
import org.koin.core.scope.Scope
|
||||||
|
import org.koin.dsl.module
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class JvmDatabaseBuilder() : PlatformDatabaseBuilder {
|
class JvmDatabaseBuilder() : PlatformDatabaseBuilder {
|
||||||
|
|
@ -18,3 +19,5 @@ class JvmDatabaseBuilder() : PlatformDatabaseBuilder {
|
||||||
|
|
||||||
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
|
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
|
||||||
JvmDatabaseBuilder()
|
JvmDatabaseBuilder()
|
||||||
|
|
||||||
|
internal actual val ExtPlatformModule = module { }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue