Skip to content

Commit

Permalink
Merge branch 'release/2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cristan committed Nov 13, 2024
2 parents 6645f98 + 95b1271 commit 1662049
Show file tree
Hide file tree
Showing 33 changed files with 517 additions and 4,195 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ This app is designed to find how many OV-fiets bikes are available as quick as p
* Show additional information like address, opening hours and other locations at the same station.

## About the code
Data comes from [openOV](https://openov.nl) combined with the total availabilty per station kindly provided by [ovfietsbeschikbaar.nl](https://ovfietsbeschikbaar.nl/).
Data comes from [openOV](https://openov.nl) and is hosted by the open source [OvFietsBackend](https://github.com/cristan/OvFietsBackend).
This data is combined with the total availability per station which is kindly provided by [ovfietsbeschikbaar.nl](https://ovfietsbeschikbaar.nl/).

The code is pretty much using the latest technologies available (at least at the time of writing)
* 100% Jetpack Compose
Expand Down
14 changes: 4 additions & 10 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ plugins {

android {
namespace = "nl.ovfietsbeschikbaarheid"
compileSdk = 34
compileSdk = 35

defaultConfig {
applicationId = "nl.ovfietsbeschikbaarheid"
minSdk = 26
targetSdk = 34
versionCode = 11
versionName = "1.2"
targetSdk = 35
versionCode = 12
versionName = "2.0"

// Only include resources for supported languages
resourceConfigurations += listOf("nl", "en")
Expand Down Expand Up @@ -48,12 +48,6 @@ android {
}
}

kotlin {
sourceSets.all {
languageSettings.enableLanguageFeature("ExplicitBackingFields")
}
}

dependencies {

implementation(libs.androidx.core.ktx)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package nl.ovfietsbeschikbaarheid

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package nl.ovfietsbeschikbaarheid

import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import nl.ovfietsbeschikbaarheid.repository.OverviewRepository
import nl.ovfietsbeschikbaarheid.repository.StationRepository
import nl.ovfietsbeschikbaarheid.util.dutchLocale
import org.junit.Test

class LocationsDataTest {

@Test
fun test() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val stationRepository = StationRepository(context)
val allStations = stationRepository.getAllStations()
val capacities = stationRepository.getCapacities()
runBlocking {
val allLocations = OverviewRepository().getAllLocations()
allLocations.forEach {
val stationName = allStations[it.stationCode]
if (stationName == null && it.stationCode != "BSLC") {
// Triggers at Eindhoven Strijp-S. This is a "new" train station since 2015, so probably from just after stations_nl_2015_08 is made.
// Same applies to Utrecht Vaartsche Rijn: this is made at 2016.
println("Station ${it.stationCode} not found for $it")
}
val foundCapacity = capacities[it.locationCode.lowercase(dutchLocale)]
if (foundCapacity == null) {
error("Capacity for ${it.locationCode} not found")
}
}

}
}
}
5 changes: 5 additions & 0 deletions app/src/main/java/nl/ovfietsbeschikbaarheid/KtorApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import nl.ovfietsbeschikbaarheid.dto.DetailsDTO
import nl.ovfietsbeschikbaarheid.dto.LocationDTO
import timber.log.Timber

class KtorApiClient {
Expand All @@ -26,6 +27,10 @@ class KtorApiClient {
}
}

suspend fun getLocations(): List<LocationDTO> {
return httpClient.get("https://storage.googleapis.com/ov-fiets-updates/locations.json").body<List<LocationDTO>>()
}

suspend fun getDetails(detailUri: String): DetailsDTO? {
Timber.i("Loading $detailUri")
val result = httpClient.get(detailUri)
Expand Down
7 changes: 0 additions & 7 deletions app/src/main/java/nl/ovfietsbeschikbaarheid/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@ package nl.ovfietsbeschikbaarheid
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import nl.ovfietsbeschikbaarheid.ui.navigation.Navigation
import nl.ovfietsbeschikbaarheid.ui.screen.AboutScreen
import nl.ovfietsbeschikbaarheid.ui.screen.DetailScreen
import nl.ovfietsbeschikbaarheid.ui.screen.HomeScreen

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/nl/ovfietsbeschikbaarheid/TestData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import nl.ovfietsbeschikbaarheid.model.LocationType
object TestData {
val testLocationOverviewModel = LocationOverviewModel(
"Amersfoort Centraal",
288,
"https://places.ns-mlab.nl/api/v2/places/stationfacility/Zelfservice%20OV-fiets%20uitgiftepunt-nvd001",
1729602804,
"nvd001",
"HVS",
latitude = 52.36599,
longitude = 6.469563,
type = LocationType.Regular
type = LocationType.Regular,
openingHours = emptyList()
)
}
10 changes: 1 addition & 9 deletions app/src/main/java/nl/ovfietsbeschikbaarheid/dto/DetailsDTO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ data class DetailsPayload(

val extra: PayloadExtra,

val openingHours: List<OpeningHours>? = null,
val openingHours: List<OpeningHoursDTO>? = null,
val infoImages: List<InfoImage>,
)

Expand All @@ -47,14 +47,6 @@ data class PayloadExtra(
val locationCode: String,
)

@Serializable
data class OpeningHours(
val dayOfWeek: Int,
val startTime: String,
val endTime: String,
val closesNextDay: Boolean,
)

@Serializable
data class InfoImage(
val title: String,
Expand Down
12 changes: 5 additions & 7 deletions app/src/main/java/nl/ovfietsbeschikbaarheid/dto/LocationsDTO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@ package nl.ovfietsbeschikbaarheid.dto
import kotlinx.serialization.Serializable

@Serializable
data class LocationsDTO(
val locaties: Map<String, Location>
)

@Serializable
data class Location(
data class LocationDTO(
val description: String,
val stationCode: String,
val lat: Double,
val lng: Double,
val extra: LocationExtra,
val link: Link
val link: Link,
// Weirdly nullable, see Ermelo
val openingHours: List<OpeningHoursDTO>? = null
)

@Serializable
Expand All @@ -26,4 +23,5 @@ data class Link(
data class LocationExtra(
val locationCode: String,
val fetchTime: Long,
val rentalBikes: Int? = null,
)
11 changes: 11 additions & 0 deletions app/src/main/java/nl/ovfietsbeschikbaarheid/dto/OpeningHoursDTO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package nl.ovfietsbeschikbaarheid.dto

import kotlinx.serialization.Serializable

@Serializable
data class OpeningHoursDTO(
val dayOfWeek: Int,
val startTime: String,
val endTime: String,
val closesNextDay: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.annotation.StringRes
import com.google.android.gms.maps.model.LatLng
import nl.ovfietsbeschikbaarheid.R
import nl.ovfietsbeschikbaarheid.dto.DetailsDTO
import nl.ovfietsbeschikbaarheid.model.DetailScreenData
import nl.ovfietsbeschikbaarheid.model.DetailsModel
import nl.ovfietsbeschikbaarheid.model.LocationModel
import nl.ovfietsbeschikbaarheid.model.LocationOverviewModel
Expand Down Expand Up @@ -64,7 +65,7 @@ object DetailsMapper {

// Don't pick yourself
it.locationCode != payload.extra.locationCode
}
}.map { DetailScreenData(it.title, it.uri, it.fetchTime) }

val foundCapacity = capacities[payload.extra.locationCode.lowercase(Locale.UK)]
if (foundCapacity == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,62 @@
package nl.ovfietsbeschikbaarheid.mapper

import dev.jordond.compass.Coordinates
import nl.ovfietsbeschikbaarheid.dto.LocationsDTO
import nl.ovfietsbeschikbaarheid.dto.LocationDTO
import nl.ovfietsbeschikbaarheid.ext.distanceTo
import nl.ovfietsbeschikbaarheid.model.LocationOverviewModel
import nl.ovfietsbeschikbaarheid.model.LocationOverviewWithDistanceModel
import nl.ovfietsbeschikbaarheid.model.LocationType
import nl.ovfietsbeschikbaarheid.util.dutchLocale
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import kotlin.math.roundToInt

object LocationsMapper {
private val nonExistingLocations = listOf(
"asb003",
"ut018",
"UTVR002",
"gvc021",
"had002",
"ed001",
"ed002",
// TODO: we might want to add "ktr001": it's added recently, but no updates since
)

fun map(locationsDTO: LocationsDTO): List<LocationOverviewModel> {
val locations = locationsDTO.locaties.values
.filter { !nonExistingLocations.contains(it.extra.locationCode) }

fun map(locations: List<LocationDTO>): List<LocationOverviewModel> {
val replacements = hashMapOf(
Pair("s-Hertogenbosch", "'s-Hertogenbosch"),
Pair("Delft, Fietsenstalling", "Delft"),
Pair("Leiden Centraal,Uitgang LUMC", "Leiden Centraal, Uitgang LUMC"),
Pair("Vianen OV-fiets", "Vianen"),

Pair("Hollandse Rading OV-fiets ", "Hollandse Rading"),
Pair("Vianen OV-fiets ", "Vianen"),
Pair("OV-fiets - Maastricht", "Maastricht"),

Pair("Openbare fietsenstalling gemeente Groningen : Fietsenstalling Europapark", "Fietsenstalling Europapark"),

// All the other locations at Utrecht start with the word Utrecht, including the other P+Rs. Use the same scheme to make sure they're sorted together.
Pair("P + R Utrecht Science Park (De Uithof)", "Utrecht P+R Science Park (De Uithof)"),
// All of these also help with the alphabetical order
Pair("OV-ebike Arnhem Centrum", "Arnhem Centrum - OV-ebike"),
Pair("OV-ebike Driebergen-Zeist", "Driebergen-Zeist - OV-ebike"),
Pair("OV-ebike Groningen", "Groningen - OV-ebike"),
Pair("OV-ebike Maastricht", "Maastricht - OV-ebike"),
Pair("OV-fiets - Maastricht", "Maastricht"),
)

return locations.map { toMap ->
val description = replacements[toMap.description] ?: toMap.description
LocationOverviewModel(
title = description,
rentalBikesAvailable = toMap.extra.rentalBikes,
uri = toMap.link.uri,
fetchTime = toMap.extra.fetchTime,
locationCode = toMap.extra.locationCode,
stationCode = toMap.stationCode,
latitude = toMap.lat,
longitude = toMap.lng,
type = if (description.contains("OV-ebike")) LocationType.EBike else LocationType.Regular
type = if (description.contains("OV-ebike")) LocationType.EBike else LocationType.Regular,
openingHours = toMap.openingHours
)
}.sortedBy { it.title }
}

fun withDistance(locations: List<LocationOverviewModel>, currentCoordinates: Coordinates): List<LocationOverviewWithDistanceModel> {
val symbols = DecimalFormatSymbols(dutchLocale)

val kmFormat = DecimalFormat().apply {
minimumFractionDigits = 1
maximumFractionDigits = 1
decimalFormatSymbols = symbols
}
return locations
.sortedBy { it.distanceTo(currentCoordinates) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package nl.ovfietsbeschikbaarheid.mapper

import nl.ovfietsbeschikbaarheid.R
import nl.ovfietsbeschikbaarheid.dto.OpeningHours
import nl.ovfietsbeschikbaarheid.dto.OpeningHoursDTO
import nl.ovfietsbeschikbaarheid.model.OpenState
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.temporal.ChronoUnit
import java.util.Locale

object OpenStateMapper {
fun getOpenState(openingHours: List<OpeningHours>, dateTime: LocalDateTime): OpenState {
fun getOpenState(openingHours: List<OpeningHoursDTO>, dateTime: LocalDateTime): OpenState {
// Check for open 24/7
if (
openingHours.all { it.startTime == "00:00" && it.endTime == "24:00" } ||
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package nl.ovfietsbeschikbaarheid.model

data class DetailScreenData(val title: String, val uri: String, val fetchTime: Long)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ data class DetailsModel(
val location: LocationModel?,
val coordinates: LatLng,
val stationName: String?,
val alternatives: List<LocationOverviewModel>,
val alternatives: List<DetailScreenData>,
)

data class LocationModel(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package nl.ovfietsbeschikbaarheid.model

import nl.ovfietsbeschikbaarheid.dto.OpeningHoursDTO

data class LocationOverviewModel(
val title: String,
val rentalBikesAvailable: Int?,
val uri: String,
val fetchTime: Long,
val locationCode: String,
val stationCode: String,
val latitude: Double,
val longitude: Double,
val type: LocationType
val type: LocationType,
val openingHours: List<OpeningHoursDTO>?
)

data class LocationOverviewWithDistanceModel(
Expand Down
Loading

0 comments on commit 1662049

Please sign in to comment.