Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ Available APIs:
- [Location Information Request](https://opentransportdata.swiss/it/cookbook/ojplocationinformationrequest-ojp-2/)
- [Trip Request](https://opentransportdata.swiss/en/cookbook/ojptriprequest/)
- [Trip Info Request](https://opentransportdata.swiss/de/cookbook/open-journey-planner-ojp/ojptripinforequest/)
- [Stop Event Request](https://opentransportdata.swiss/de/cookbook/open-journey-planner-ojp-landing-page/ojpstopeventrequest-2-0/)
- Trip Refinement Request

Coming soon:
- [Stop Event Request](https://opentransportdata.swiss/en/cookbook/ojp-stopeventservice/)

## Requirements
Compatible with Android 8+

Expand Down Expand Up @@ -168,6 +166,34 @@ requestTripInfo(
)
```

#### Get stop events (departures/arrivals) for a stop
```
import ch.opentransportdata.ojp.OjpSdk

requestStopEvent(
languageCode = LanguageCode.EN,
location = LocationDto(
placeReference = PlaceReferenceDto(
ref = "8507000",
stationName = NameDto(text = "Bern"),
position = null
)
),
params = StopEventParam(
numberOfResults = 10,
stopEventType = StopEventType.DEPARTURE,
includePreviousCalls = false,
includeOnwardCalls = true,
includeOperatingDays = true,
useRealtimeData = RealtimeData.FULL,
modeFilter = ModeFilter(
ptMode = listOf(PtMode.RAIL),
exclude = false
)
)
)
```

#### Refine a trip
```
import ch.opentransportdata.ojp.OjpSdk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,17 @@ import ch.opentransportdata.ojp.BuildConfig
import ch.opentransportdata.ojp.OjpSdk
import ch.opentransportdata.ojp.data.dto.response.GeoPositionDto
import ch.opentransportdata.ojp.data.dto.response.PlaceResultDto
import androidx.navigation.NavHostController
import ch.opentransportdata.presentation.feature.location.LirScreenComposable
import ch.opentransportdata.presentation.feature.map.MapScreen
import ch.opentransportdata.presentation.feature.result.TripResultScreen
import ch.opentransportdata.presentation.feature.search.TripSearchScreen
import ch.opentransportdata.presentation.feature.stopevent.StopEventResultScreen
import ch.opentransportdata.presentation.feature.stopevent.StopEventSearchScreen
import ch.opentransportdata.presentation.navigation.BottomNavItem
import ch.opentransportdata.presentation.navigation.LocationSearchMask
import ch.opentransportdata.presentation.navigation.StopEventResults
import ch.opentransportdata.presentation.navigation.StopEventSearchMask
import ch.opentransportdata.presentation.navigation.TripMap
import ch.opentransportdata.presentation.navigation.TripResults
import ch.opentransportdata.presentation.navigation.TripSearchMask
Expand All @@ -49,7 +54,7 @@ class MainActivity : ComponentActivity() {

@Composable
fun OjpDemoApp() {
val bottomNavigationItems = listOf(BottomNavItem.Lir, BottomNavItem.Tir)
val bottomNavigationItems = listOf(BottomNavItem.Lir, BottomNavItem.Tir, BottomNavItem.Ser)
OJPAndroidSDKTheme {
val navController = rememberNavController()
var selectedBottomNavItem by remember { mutableIntStateOf(0) }
Expand Down Expand Up @@ -87,6 +92,7 @@ class MainActivity : ComponentActivity() {
) {
composable<BottomNavItem.Lir> { LirNavHost() }
composable<BottomNavItem.Tir> { TirNavHost() }
composable<BottomNavItem.Ser> { SerNavHost() }
}
}
}
Expand Down Expand Up @@ -131,6 +137,24 @@ class MainActivity : ComponentActivity() {
}
}

@Composable
private fun SerNavHost() {
val navController: NavHostController = rememberNavController()

NavHost(navController = navController, startDestination = StopEventSearchMask) {
composable<StopEventSearchMask> { StopEventSearchScreen(navHostController = navController) }
composable<StopEventResults>(
typeMap = mapOf(
typeOf<PlaceResultDto?>() to PlaceResultType,
typeOf<PlaceResultDto>() to PlaceResultType,
)
) { navBackstackEntry ->
val parameters = navBackstackEntry.toRoute<StopEventResults>()
StopEventResultScreen(stop = parameters.stop)
}
}
}

companion object {
val ojpSdk = OjpSdk(
baseUrl = "https://odpch-api.clients.liip.ch/",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package ch.opentransportdata.presentation.feature.stopevent

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import ch.opentransportdata.ojp.data.dto.response.PlaceResultDto
import ch.opentransportdata.ojp.data.dto.response.ser.StopEventResultDto
import ch.opentransportdata.ojp.domain.model.StopEventType
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

/**
* Created by Deniz Kalem on 20.05.2026
*/
@Composable
fun StopEventResultScreen(
modifier: Modifier = Modifier,
stop: PlaceResultDto,
viewModel: StopEventResultViewModel = viewModel(),
) {
val state = viewModel.state.collectAsState()
val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()

LaunchedEffect(stop) {
viewModel.load(stop, StopEventType.DEPARTURE)
}

Scaffold(
contentWindowInsets = WindowInsets.statusBars,
snackbarHost = { SnackbarHost(hostState = snackBarHostState) }
) { innerPadding ->
Column(
modifier = modifier
.padding(innerPadding)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Text(
modifier = Modifier.padding(top = 16.dp),
text = state.value.stopName ?: stop.place?.stopPlace?.name?.text ?: "Stop events",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)

Spacer(Modifier.size(12.dp))

Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(
selected = state.value.stopEventType == StopEventType.DEPARTURE,
onClick = {
if (state.value.stopEventType != StopEventType.DEPARTURE) {
viewModel.toggleStopEventType(stop)
}
},
label = { Text("Departures") }
)
FilterChip(
selected = state.value.stopEventType == StopEventType.ARRIVAL,
onClick = {
if (state.value.stopEventType != StopEventType.ARRIVAL) {
viewModel.toggleStopEventType(stop)
}
},
label = { Text("Arrivals") }
)
}

Spacer(Modifier.size(12.dp))

when {
state.value.isLoading -> Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}

state.value.results.isEmpty() -> Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) { Text("No stop events found") }

else -> LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(state.value.results, key = { it.id }) { result ->
StopEventRow(result, state.value.stopEventType)
HorizontalDivider()
}
}
}
}
}

state.value.events.forEach { event ->
when (event) {
is StopEventResultViewModel.Event.ShowSnackBar -> {
coroutineScope.launch {
snackBarHostState.showSnackbar(event.message)
}
}
}
viewModel.eventHandled(event.id)
}
}

@Composable
private fun StopEventRow(result: StopEventResultDto, stopEventType: StopEventType) {
val stopEvent = result.stopEvent
val call = stopEvent.thisCall.callAtStop
val service = stopEvent.service

val time = when (stopEventType) {
StopEventType.ARRIVAL -> call.serviceArrival?.timetabledTime
else -> call.serviceDeparture?.timetabledTime
}
val estimatedTime = when (stopEventType) {
StopEventType.ARRIVAL -> call.serviceArrival?.estimatedTime
else -> call.serviceDeparture?.estimatedTime
}
val plannedQuay = call.plannedQuay?.text
val estimatedQuay = call.estimatedQuay?.text

Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = formatTime(time),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
if (estimatedTime != null && estimatedTime != time) {
Text(
text = formatTime(estimatedTime),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.tertiary
)
}
}

Spacer(Modifier.width(12.dp))

Box(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Text(
text = service.publishedServiceName.text ?: "",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onPrimaryContainer,
fontWeight = FontWeight.Bold
)
}

Spacer(Modifier.width(12.dp))

Column(modifier = Modifier.weight(1f)) {
Text(
text = service.destinationText?.text ?: service.originText.text ?: "",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
service.mode.name?.text?.let { Text(it, style = MaterialTheme.typography.bodySmall) }
}

if (plannedQuay != null) {
Text(
text = "Pl. ${estimatedQuay ?: plannedQuay}",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium
)
}
}
}

private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")

private fun formatTime(time: LocalDateTime?): String {
return time?.format(timeFormatter) ?: "--:--"
}
Loading
Loading