1
1
mirror of https://github.com/Pygmalion69/OpenTopoMapViewer.git synced 2025-10-06 00:02:42 +02:00

Merge branch “feature-ors”

This commit is contained in:
Pygmalion69
2025-08-30 12:44:16 +02:00
85 changed files with 2438 additions and 717 deletions

View File

@@ -45,15 +45,16 @@ import org.nitri.opentopo.viewmodel.GpxViewModel
import org.nitri.opentopo.nearby.NearbyFragment
import org.nitri.opentopo.nearby.entity.NearbyItem
import org.nitri.opentopo.util.Utils
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.client.OpenRouteServiceClient
import org.osmdroid.util.GeoPoint
import androidx.core.net.toUri
import org.nitri.ors.DefaultOrsClient
import org.nitri.ors.Ors
import org.nitri.ors.OrsClient
open class BaseMainActivity : AppCompatActivity(), MapFragment.OnFragmentInteractionListener,
GpxDetailFragment.OnFragmentInteractionListener, NearbyFragment.OnFragmentInteractionListener {
private val parser = GPXParser()
private var openRouteServiceApi: OpenRouteServiceApi? = null
private var orsClient: OrsClient? = null
private var geoPointFromIntent: GeoPointDto? = null
private var gpxUriString: String? = null
private var gpxUri: Uri? = null
@@ -83,7 +84,7 @@ open class BaseMainActivity : AppCompatActivity(), MapFragment.OnFragmentInterac
private val orsApiKeyChangesReceiver = object: BroadcastReceiver() {
override fun onReceive(p0: Context?, intent: Intent?) {
createOrsApi()
createOrsClient()
}
}
@@ -95,8 +96,8 @@ open class BaseMainActivity : AppCompatActivity(), MapFragment.OnFragmentInterac
// NOP
}
override fun getOpenRouteServiceApi(): OpenRouteServiceApi? {
return openRouteServiceApi
override fun getOpenRouteServiceClient(): OrsClient? {
return orsClient
}
private lateinit var sharedPreferences: SharedPreferences
@@ -182,13 +183,13 @@ open class BaseMainActivity : AppCompatActivity(), MapFragment.OnFragmentInterac
// Log.d("ORS", "Distance: ${result.routes.firstOrNull()?.summary?.distance} m")
// }
createOrsApi()
createOrsClient()
}
private fun createOrsApi() {
private fun createOrsClient() {
val apiKey = sharedPreferences.getString(PREF_ORS_API_KEY, "")
if (apiKey?.isNotEmpty() == true) {
openRouteServiceApi = OpenRouteServiceClient.create(apiKey, this@BaseMainActivity)
orsClient = Ors.create(apiKey, applicationContext)
}
}

View File

@@ -52,7 +52,7 @@ import org.nitri.opentopo.util.OrientationSensor
import org.nitri.opentopo.util.Utils
import org.nitri.opentopo.viewmodel.LocationViewModel
import org.nitri.opentopo.viewmodel.MarkerViewModel
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.OrsClient
import org.osmdroid.config.Configuration
import org.osmdroid.events.DelayedMapListener
import org.osmdroid.events.MapEventsReceiver
@@ -398,10 +398,10 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
}
val locale = Resources.getSystem().configuration.locales.get(0)
val language = locale.toLanguageTag().lowercase()
listener?.getOpenRouteServiceApi()?.let { api ->
listener?.getOpenRouteServiceClient()?.let { client ->
val profile = sharedPreferences.getString(PREF_ORS_PROFILE, "driving-car")
profile?.let {
val directions = Directions(api, it)
val directions = Directions(client, it)
directions.getRouteGpx(coordinates, language, object : Directions.RouteGpResult {
override fun onSuccess(gpx: String) {
Log.d(TAG, "GPX: $gpx")
@@ -1060,9 +1060,9 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
fun showPrivacyOptionsForm()
/**
* Get the ORS API if available
* Get the ORS client if available
*/
fun getOpenRouteServiceApi(): OpenRouteServiceApi?
fun getOpenRouteServiceClient(): OrsClient?
/**
* Parse GPX string

View File

@@ -4,17 +4,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.repository.RouteRepository
import org.nitri.ors.OrsClient
import org.nitri.ors.helper.RouteHelper
class Directions(val api: OpenRouteServiceApi, private val profile: String) {
class Directions(val client: OrsClient, private val profile: String) {
val repository = RouteRepository(api)
val routeHelper = RouteHelper()
fun getRouteGpx(coordinates: List<List<Double>>, language: String, result: RouteGpResult) {
CoroutineScope(Dispatchers.IO).launch {
try {
val gpxXml = repository.getRouteGpx(coordinates, language, profile)
val gpxXml = with(routeHelper) { client.getRouteGpx(coordinates, language, profile) }
withContext(Dispatchers.Main) {
if (gpxXml.isNotBlank()) {
result.onSuccess(gpxXml)

View File

@@ -9,7 +9,7 @@ plugins {
android {
namespace = "org.nitri.ors"
compileSdk = 35
compileSdk = 36
defaultConfig {
minSdk = 24
@@ -42,6 +42,8 @@ android {
lint {
targetSdk = 35
}
publishing { singleVariant("release") { withSourcesJar() } }
}
dependencies {

View File

@@ -0,0 +1,65 @@
package org.nitri.ors
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.elevation.ElevationLineRequestBuilderJ
import org.nitri.ors.domain.elevation.ElevationPointRequestBuilderJ
import org.nitri.ors.domain.elevation.ElevationFormats
import org.nitri.ors.domain.elevation.elevationLineRequest
import org.nitri.ors.domain.elevation.elevationPointRequest
@RunWith(AndroidJUnit4::class)
class ElevationDslInstrumentedTest {
@Test
fun dsl_buildsElevationLine_polyline() {
val req = elevationLineRequest {
polyline(8.681495 to 49.41461, 8.687872 to 49.420318)
formatOut = ElevationFormats.GEOJSON
}
assertEquals(ElevationFormats.POLYLINE, req.formatIn)
assertEquals(ElevationFormats.GEOJSON, req.formatOut)
}
@Test
fun dsl_requires_geometry_for_elevationLine() {
assertThrows(IllegalArgumentException::class.java) {
elevationLineRequest { formatIn = ElevationFormats.POLYLINE }
}
}
@Test
fun dsl_buildsElevationPoint_point() {
val req = elevationPointRequest {
point(8.681495, 49.41461)
formatIn = "point"
formatOut = "geojson"
}
assertEquals("geojson", req.formatOut)
assertEquals("point", req.formatIn)
}
@Test
fun javaBuilder_buildsElevationLine_polyline() {
val req = ElevationLineRequestBuilderJ()
.polyline(listOf(listOf(8.0,48.0), listOf(9.0,49.0)))
.formatOut(ElevationFormats.GEOJSON)
.build()
assertEquals(ElevationFormats.POLYLINE, req.formatIn)
}
@Test
fun javaBuilder_buildsElevationPoint_point() {
val req = ElevationPointRequestBuilderJ()
.point(8.0, 48.0)
.formatOut("geojson")
.build()
assertEquals("geojson", req.formatOut)
}
}

View File

@@ -9,28 +9,28 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.repository.ElevationRepository
import org.nitri.ors.helper.ElevationHelper
@RunWith(AndroidJUnit4::class)
class ElevationInstrumentedTest {
private fun createRepository(context: Context): ElevationRepository {
private fun create(context: Context): Pair<DefaultOrsClient, ElevationHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return ElevationRepository(api)
val client = DefaultOrsClient(apiKey, context)
val helper = ElevationHelper()
return client to helper
}
@Test
fun testElevation_point_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, helper) = create(context)
// A point near Heidelberg, Germany
val lon = 8.681495
val lat = 49.41461
val response = repository.getElevationPoint(lon = lon, lat = lat)
val response = with(helper) { client.getElevationPoint(lon = lon, lat = lat) }
assertNotNull("Elevation point response should not be null", response)
assertNotNull("Geometry should not be null", response.geometry)
@@ -49,7 +49,7 @@ class ElevationInstrumentedTest {
@Test
fun testElevation_line_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, helper) = create(context)
// A short line segment around Heidelberg
val coordinates = listOf(
@@ -57,7 +57,7 @@ class ElevationInstrumentedTest {
listOf(8.687872, 49.420318)
)
val response = repository.getElevationLine(coordinates = coordinates)
val response = with(helper) { client.getElevationLine(coordinates = coordinates) }
assertNotNull("Elevation line response should not be null", response)
assertEquals("Geometry type should be LineString", "LineString", response.geometry.type)

View File

@@ -0,0 +1,42 @@
package org.nitri.ors
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.export.ExportRequestBuilderJ
import org.nitri.ors.domain.export.exportRequest
@RunWith(AndroidJUnit4::class)
class ExportDslInstrumentedTest {
@Test
fun dsl_buildsExportRequest_andValidates() {
val req = exportRequest {
bbox(8.681495, 49.41461, 8.686507, 49.41943)
id("test-id")
geometry(true)
}
assertEquals("test-id", req.id)
assertEquals(listOf(listOf(8.681495,49.41461), listOf(8.686507,49.41943)), req.bbox)
}
@Test
fun dsl_requires_bbox() {
assertThrows(IllegalArgumentException::class.java) {
exportRequest { geometry(false) }
}
}
@Test
fun javaBuilder_buildsExportRequest_andValidates() {
val req = ExportRequestBuilderJ()
.bbox(8.0, 48.0, 9.0, 49.0)
.id("jid")
.geometry(null)
.build()
assertEquals("jid", req.id)
assertEquals(listOf(listOf(8.0,48.0), listOf(9.0,49.0)), req.bbox)
}
}

View File

@@ -0,0 +1,75 @@
package org.nitri.ors
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.helper.ExportHelper
@RunWith(AndroidJUnit4::class)
class ExportInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, ExportHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val helper = ExportHelper()
return client to helper
}
@Test
fun testExport_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, helper) = create(context)
// Bounding box around Heidelberg, Germany
val bbox = listOf(
listOf(8.681495, 49.41461), // minLon, minLat
listOf(8.686507, 49.41943) // maxLon, maxLat
)
val response = with(helper) { client.export(bbox = bbox, profile = Profile.DRIVING_CAR) }
assertNotNull("Export response should not be null", response)
// Basic sanity checks on structure
assertTrue("Points list should not be empty", response.nodes.isNotEmpty())
assertTrue("Edges list should not be empty", response.edges.isNotEmpty())
}
// Exceeds rate limit
// @Test
// fun testExportJson_successful() = runBlocking {
// val context = ApplicationProvider.getApplicationContext<Context>()
//
// val (client, helper) = create(context)
// val bbox = listOf(
// listOf(8.681495, 49.41461),
// listOf(8.686507, 49.41943)
// )
//
// val response = with(helper) { client.exportJson(bbox = bbox, profile = "driving-car") }
//
// assertNotNull("JSON Export response should not be null", response)
// assertTrue("Points list should not be empty", response.nodes.isNotEmpty())
// }
// Exceeds rate limit
// @Test
// fun testExportTopoJson_successful() = runBlocking {
// val context = ApplicationProvider.getApplicationContext<Context>()
// val (client, helper) = create(context)
//
// val bbox = listOf(
// listOf(8.681495, 49.41461),
// listOf(8.686507, 49.41943)
// )
//
// val topo = with(helper) { client.exportTopoJson(bbox = bbox, profile = "driving-car") }
//
// assertNotNull("TopoJSON Export response should not be null", topo)
// assertTrue("Type should be present", topo.type.isNotBlank())
// }
}

View File

@@ -1,75 +0,0 @@
package org.nitri.ors
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.model.export.ExportRequest
import org.nitri.ors.repository.ExportRepository
@RunWith(AndroidJUnit4::class)
class ExportRepositoryInstrumentedTest {
private fun createRepository(context: Context): ExportRepository {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return ExportRepository(api)
}
@Test
fun testExport_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
// Bounding box around Heidelberg, Germany
val bbox = listOf(
listOf(8.681495, 49.41461), // minLon, minLat
listOf(8.686507, 49.41943) // maxLon, maxLat
)
val response = repository.export(bbox = bbox, profile ="driving-car")
assertNotNull("Export response should not be null", response)
// Basic sanity checks on structure
assertTrue("Points list should not be empty", response.nodes.isNotEmpty())
assertTrue("Edges list should not be empty", response.edges.isNotEmpty())
}
@Test
fun testExportJson_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val bbox = listOf(
listOf(8.681495, 49.41461),
listOf(8.686507, 49.41943)
)
val response = repository.exportJson(bbox = bbox, profile ="driving-car")
assertNotNull("JSON Export response should not be null", response)
assertTrue("Points list should not be empty", response.nodes.isNotEmpty())
}
@Test
fun testExportTopoJson_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val bbox = listOf(
listOf(8.681495, 49.41461),
listOf(8.686507, 49.41943)
)
val topo = repository.exportTopoJson(bbox = bbox, profile = "driving-car")
assertNotNull("TopoJSON Export response should not be null", topo)
assertTrue("Type should be present", topo.type.isNotBlank())
}
}

View File

@@ -8,29 +8,31 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.repository.GeocodeRepository
import org.nitri.ors.helper.GeocodeHelper
@RunWith(AndroidJUnit4::class)
class GeocodeInstrumentedTest {
private fun createRepository(context: Context): GeocodeRepository {
private fun create(context: Context): Pair<DefaultOrsClient, GeocodeHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return GeocodeRepository(api)
val client = DefaultOrsClient(apiKey, context)
val helper = GeocodeHelper()
return client to helper
}
@Test
fun testGeocode_search_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repo = createRepository(context)
val (client, helper) = create(context)
val apiKey = context.getString(R.string.ors_api_key)
val response = repo.search(
text = "Heidelberg",
apiKey = apiKey,
size = 5
)
val response = with(helper) {
client.search(
text = "Heidelberg",
apiKey = apiKey,
size = 5
)
}
assertNotNull("Search response should not be null", response)
assertTrue("Features should not be empty for Heidelberg search", response.features.isNotEmpty())
@@ -49,19 +51,21 @@ class GeocodeInstrumentedTest {
@Test
fun testGeocode_reverse_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repo = createRepository(context)
val (client, helper) = create(context)
val apiKey = context.getString(R.string.ors_api_key)
// Point near Heidelberg, Germany
val lon = 8.681495
val lat = 49.41461
val response = repo.reverse(
apiKey = apiKey,
lon = lon,
lat = lat,
size = 5
)
val response = with(helper) {
client.reverse(
apiKey = apiKey,
lon = lon,
lat = lat,
size = 5
)
}
assertNotNull("Reverse response should not be null", response)
assertTrue("Reverse should return at least one feature", response.features.isNotEmpty())
@@ -74,14 +78,16 @@ class GeocodeInstrumentedTest {
@Test
fun testGeocode_autocomplete_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repo = createRepository(context)
val (client, helper) = create(context)
val apiKey = context.getString(R.string.ors_api_key)
val response = repo.autocomplete(
apiKey = apiKey,
text = "Heidelb",
size = 5
)
val response = with(helper) {
client.autocomplete(
apiKey = apiKey,
text = "Heidelb",
size = 5
)
}
assertNotNull("Autocomplete response should not be null", response)
assertTrue("Autocomplete should return suggestions", response.features.isNotEmpty())

View File

@@ -0,0 +1,49 @@
package org.nitri.ors
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.isochrones.IsochronesRequestBuilderJ
import org.nitri.ors.domain.isochrones.isochronesRequest
@RunWith(AndroidJUnit4::class)
class IsochronesDslInstrumentedTest {
@Test
fun dsl_buildsIsochronesRequest_andValidates() {
val req = isochronesRequest {
location(8.681495, 49.41461)
rangeSeconds(600)
rangeType = "time"
intersections = true
}
assertEquals(listOf(listOf(8.681495,49.41461)), req.locations)
assertEquals(listOf(600), req.range)
assertEquals("time", req.rangeType)
assertEquals(true, req.intersections)
}
@Test
fun dsl_requires_location_and_range() {
assertThrows(IllegalArgumentException::class.java) {
isochronesRequest { rangeSeconds(300) }
}
assertThrows(IllegalArgumentException::class.java) {
isochronesRequest { location(8.0, 48.0) }
}
}
@Test
fun javaBuilder_buildsIsochronesRequest_andValidates() {
val req = IsochronesRequestBuilderJ()
.location(8.0, 48.0)
.addRange(900)
.rangeType("time")
.intersections(false)
.build()
assertEquals(listOf(listOf(8.0,48.0)), req.locations)
assertEquals(listOf(900), req.range)
}
}

View File

@@ -8,22 +8,22 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.repository.IsochronesRepository
import org.nitri.ors.helper.IsochronesHelper
@RunWith(AndroidJUnit4::class)
class IsochronesInstrumentedTest {
private fun createRepository(context: Context): IsochronesRepository {
private fun create(context: Context): Pair<DefaultOrsClient, IsochronesHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return IsochronesRepository(api)
val client = DefaultOrsClient(apiKey, context)
val helper = IsochronesHelper()
return client to helper
}
@Test
fun testIsochrones_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, helper) = create(context)
// Heidelberg, Germany [lon, lat]
val locations = listOf(
@@ -31,15 +31,17 @@ class IsochronesInstrumentedTest {
)
// 5 minutes (300 seconds)
val range = listOf(300)
val profile = "driving-car"
val profile = Profile.DRIVING_CAR
val response = repository.getIsochrones(
locations = locations,
range = range,
profile = profile,
attributes = null,
rangeType = "time"
)
val response = with(helper) {
client.getIsochrones(
locations = locations,
range = range,
profile = profile,
attributes = null,
rangeType = "time"
)
}
assertNotNull("Isochrones response should not be null", response)
assertTrue("Features should not be empty", response.features.isNotEmpty())

View File

@@ -0,0 +1,46 @@
package org.nitri.ors
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.matrix.MatrixRequestBuilderJ
import org.nitri.ors.domain.matrix.matrixRequest
@RunWith(AndroidJUnit4::class)
class MatrixDslInstrumentedTest {
@Test
fun dsl_buildsMatrixRequest_andValidates() {
val req = matrixRequest {
location(8.681495, 49.41461)
location(8.686507, 49.41943)
metrics = listOf("duration", "distance")
resolveLocations = false
}
assertEquals(2, req.locations.size)
assertEquals(listOf(8.681495,49.41461), req.locations.first())
assertEquals(listOf("duration","distance"), req.metrics)
}
@Test
fun dsl_requires_location() {
assertThrows(IllegalArgumentException::class.java) {
matrixRequest { /* no locations */ }
}
}
@Test
fun javaBuilder_buildsMatrixRequest_andValidates() {
val req = MatrixRequestBuilderJ()
.location(8.0, 48.0)
.location(9.0, 49.0)
.metrics(listOf("duration"))
.resolveLocations(true)
.build()
assertEquals(2, req.locations.size)
assertEquals(listOf(8.0,48.0), req.locations.first())
assertEquals(listOf("duration"), req.metrics)
}
}

View File

@@ -9,37 +9,39 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.repository.MatrixRepository
import org.nitri.ors.helper.MatrixHelper
@RunWith(AndroidJUnit4::class)
class MatrixInstrumentedTest {
private fun createRepository(context: Context): MatrixRepository {
private fun create(context: Context): Pair<DefaultOrsClient, MatrixHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return MatrixRepository(api)
val client = DefaultOrsClient(apiKey, context)
val helper = MatrixHelper()
return client to helper
}
@Test
fun testMatrix_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, helper) = create(context)
// Two locations in/near Heidelberg, Germany [lon, lat]
val locations = listOf(
listOf(8.681495, 49.41461), // Heidelberg center
listOf(8.687872, 49.420318) // Nearby point
)
val profile = "driving-car"
val profile = Profile.DRIVING_CAR
val metrics = listOf("duration", "distance")
val response = repository.getMatrix(
locations = locations,
profile = profile,
metrics = metrics,
resolveLocations = false
)
val response = with(helper) {
client.getMatrix(
locations = locations,
profile = profile,
metrics = metrics,
resolveLocations = false
)
}
assertNotNull("Matrix response should not be null", response)

View File

@@ -0,0 +1,43 @@
package org.nitri.ors
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.optimization.OptimizationRequestBuilderJ
import org.nitri.ors.domain.optimization.optimizationRequest
@RunWith(AndroidJUnit4::class)
class OptimizationDslInstrumentedTest {
@Test
fun dsl_buildsOptimizationRequest_andValidates() {
val req = optimizationRequest {
vehicle(id = 1, profile = "driving-car", startLon = 8.68, startLat = 49.41)
job(id = 10, locationLon = 8.69, locationLat = 49.42, service = 300, priority = 50)
}
assertEquals(1, req.vehicles.size)
assertEquals(1, req.jobs?.size)
}
@Test
fun dsl_requires_vehicle_and_job_or_shipment() {
assertThrows(IllegalStateException::class.java) {
optimizationRequest { job(id = 10, locationLon = 8.0, locationLat = 48.0) }
}
assertThrows(IllegalStateException::class.java) {
optimizationRequest { vehicle(id = 1, profile = "driving-car") }
}
}
@Test
fun javaBuilder_buildsOptimizationRequest_andValidates() {
val req = OptimizationRequestBuilderJ()
.addVehicle(1, "driving-car", 8.0, 48.0, null, null, null)
.addJob(10, 8.1, 48.1, 120, 10)
.build()
assertEquals(1, req.vehicles.size)
assertEquals(1, req.jobs?.size)
}
}

View File

@@ -8,24 +8,24 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.model.optimization.Job
import org.nitri.ors.model.optimization.Vehicle
import org.nitri.ors.repository.OptimizationRepository
import org.nitri.ors.domain.optimization.Job
import org.nitri.ors.domain.optimization.Vehicle
import org.nitri.ors.helper.OptimizationHelper
@RunWith(AndroidJUnit4::class)
class OptimizationInstrumentedTest {
private fun createRepository(context: Context): OptimizationRepository {
private fun create(context: Context): Pair<DefaultOrsClient, OptimizationHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return OptimizationRepository(api)
val client = DefaultOrsClient(apiKey, context)
val helper = OptimizationHelper()
return client to helper
}
@Test
fun testOptimization_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, helper) = create(context)
// Simple scenario in/near Heidelberg, Germany
val vehicle = Vehicle(
@@ -47,10 +47,12 @@ class OptimizationInstrumentedTest {
)
)
val response = repository.getOptimization(
vehicles = listOf(vehicle),
jobs = jobs
)
val response = with(helper) {
client.getOptimization(
vehicles = listOf(vehicle),
jobs = jobs
)
}
// Basic assertions
assertNotNull("Optimization response should not be null", response)

View File

@@ -0,0 +1,34 @@
package org.nitri.ors
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.pois.PoisRequestBuilderJ
import org.nitri.ors.domain.pois.poisRequest
@RunWith(AndroidJUnit4::class)
class PoisDslInstrumentedTest {
@Test
fun dsl_buildsPoisRequest_bbox() {
val req = poisRequest {
bbox(8.67, 49.40, 8.70, 49.43)
limit = 10
sortby = "distance"
}
assertEquals(10, req.limit)
assertEquals("distance", req.sortby)
requireNotNull(req.geometry.bbox)
}
@Test
fun javaBuilder_buildsPoisRequest_point() {
val req = PoisRequestBuilderJ()
.point(8.68, 49.41)
.limit(5)
.build()
assertEquals(5, req.limit)
requireNotNull(req.geometry.geojson)
}
}

View File

@@ -9,47 +9,50 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.repository.PoisRepository
import org.nitri.ors.helper.PoisHelper
@RunWith(AndroidJUnit4::class)
//@RunWith(AndroidJUnit4::class)
class PoisInstrumentedTest {
private fun createRepository(context: Context): PoisRepository {
private fun create(context: Context): Pair<DefaultOrsClient, PoisHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return PoisRepository(api)
val client = DefaultOrsClient(apiKey, context)
val helper = PoisHelper()
return client to helper
}
@Test
fun testPois_byBbox_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
// Bounding box around Heidelberg, Germany
val bbox = listOf(
listOf(8.67, 49.40), // minLon, minLat
listOf(8.70, 49.43) // maxLon, maxLat
)
val response = repository.getPoisByBbox(
bbox = bbox,
limit = 10
)
assertNotNull("POIs response should not be null", response)
assertEquals("GeoJSON type should be FeatureCollection", "FeatureCollection", response.type)
assertNotNull("Information block should be present", response.information)
assertTrue("Features should not be empty in a city bbox", response.features.isNotEmpty())
val first = response.features.first()
assertEquals("Feature type should be Feature", "Feature", first.type)
assertEquals("Geometry type should be Point", "Point", first.geometry.type)
assertEquals("Point coordinates should be [lon, lat]", 2, first.geometry.coordinates.size)
// Basic properties sanity
val props = first.properties
assertTrue("OSM id should be positive", props.osmId > 0)
assertTrue("Distance should be non-negative", props.distance >= 0.0)
}
// Leave it out for now: https://github.com/GIScience/openpoiservice/issues/150
// @Test
// fun testPois_byBbox_successful() = runBlocking {
// val context = ApplicationProvider.getApplicationContext<Context>()
// val (client, helper) = create(context)
//
// // Bounding box around Heidelberg, Germany
// val bbox = listOf(
// listOf(8.67, 49.40), // minLon, minLat
// listOf(8.70, 49.43) // maxLon, maxLat
// )
//
// val response = with(helper) {
// client.getPoisByBbox(
// bbox = bbox,
// limit = 10
// )
// }
//
// assertNotNull("POIs response should not be null", response)
// assertEquals("GeoJSON type should be FeatureCollection", "FeatureCollection", response.type)
// assertNotNull("Information block should be present", response.information)
// assertTrue("Features should not be empty in a city bbox", response.features.isNotEmpty())
//
// val first = response.features.first()
// assertEquals("Feature type should be Feature", "Feature", first.type)
// assertEquals("Geometry type should be Point", "Point", first.geometry.type)
// assertEquals("Point coordinates should be [lon, lat]", 2, first.geometry.coordinates.size)
//
// // Basic properties sanity
// val props = first.properties
// assertTrue("OSM id should be positive", props.osmId > 0)
// assertTrue("Distance should be non-negative", props.distance >= 0.0)
// }
}

View File

@@ -0,0 +1,58 @@
package org.nitri.ors
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.route.routeRequest
@RunWith(AndroidJUnit4::class)
class RouteDslInstrumentedTest {
@Test
fun buildRouteRequest_withDsl_buildsCoordinatesAndLanguage() {
val req = routeRequest {
language = "en"
start(8.68, 49.41)
coordinate(8.69, 49.42)
end(8.70, 49.43)
}
assertEquals(3, req.coordinates.size)
assertEquals(listOf(8.68, 49.41), req.coordinates.first())
assertEquals(listOf(8.70, 49.43), req.coordinates.last())
assertEquals("en", req.language)
}
@Test
fun buildRouteRequest_withDsl_throwsIfLessThanTwoPoints() {
assertThrows(IllegalArgumentException::class.java) {
routeRequest {
start(8.68, 49.41)
}
}
}
@Test
fun dslIntegration_getRoute_succeedsBasic() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val req = routeRequest {
// Very short route to minimize load
start(8.681495, 49.41461)
end(8.686507, 49.41943)
language = "en"
}
val response = client.getRoute(Profile.DRIVING_CAR, req)
assertNotNull(response)
// basic sanity: at least one route and segments present
require(response.routes.isNotEmpty())
}
}

View File

@@ -4,34 +4,31 @@ import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import okhttp3.ResponseBody
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.model.route.GeoJsonRouteResponse
import org.nitri.ors.repository.ExportRepository
import org.nitri.ors.repository.RouteRepository
import retrofit2.Response
import org.nitri.ors.domain.route.GeoJsonRouteResponse
import org.nitri.ors.helper.RouteHelper
@RunWith(AndroidJUnit4::class)
class RouteRepositoryInstrumentedTest {
class RouteInstrumentedTest {
private fun createRepository(context: Context): RouteRepository {
private fun create(context: Context): Pair<DefaultOrsClient, RouteHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return RouteRepository(api)
val client = DefaultOrsClient(apiKey, context)
val repo = RouteHelper()
return client to repo
}
@Test
fun testFetchRoute_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, repository) = create(context)
val start = Pair(8.681495, 49.41461)
val end = Pair(8.687872, 49.420318)
val route = repository.getRoute(start, end, "driving-car")
val route = with(repository) { client.getRoute(start, end, "driving-car") }
assertNotNull("Route should not be null", route)
}
@@ -39,12 +36,12 @@ class RouteRepositoryInstrumentedTest {
@Test
fun testFetchGpxRoute_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, repository) = create(context)
val start = Pair(8.681495, 49.41461)
val end = Pair(8.687872, 49.420318)
val gpxXml = repository.getRouteGpx(start, end,"driving-car")
val gpxXml = with(repository) { client.getRouteGpx(start, end, "driving-car") }
assertNotNull("GPX response body should not be null", gpxXml)
assert(gpxXml.contains("<gpx")) { "Response does not appear to be valid GPX" }
@@ -53,12 +50,13 @@ class RouteRepositoryInstrumentedTest {
@Test
fun testFetchGeoJsonRoute_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, repository) = create(context)
val start = Pair(8.681495, 49.41461)
val end = Pair(8.687872, 49.420318)
val route: GeoJsonRouteResponse = repository.getRouteGeoJson(start, end, "driving-car")
val route: GeoJsonRouteResponse = with(repository) { client.getRouteGeoJson(start, end,
Profile.DRIVING_CAR) }
assertNotNull("Route should not be null", route)
}

View File

@@ -0,0 +1,57 @@
package org.nitri.ors
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.route.RouteRequestBuilderJ
@RunWith(AndroidJUnit4::class)
class RouteRequestBuilderJInstrumentedTest {
@Test
fun builder_buildsCoordinatesAndLanguage() {
val req = RouteRequestBuilderJ()
.start(8.68, 49.41)
.add(8.69, 49.42)
.end(8.70, 49.43)
.language("de")
.build()
assertEquals(3, req.coordinates.size)
assertEquals(listOf(8.68, 49.41), req.coordinates.first())
assertEquals(listOf(8.70, 49.43), req.coordinates.last())
assertEquals("de", req.language)
}
@Test
fun builder_throwsIfLessThanTwoPoints() {
assertThrows(IllegalArgumentException::class.java) {
RouteRequestBuilderJ()
.start(8.68, 49.41)
.build()
}
}
@Test
fun builderIntegration_getRoute_succeedsBasic() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val req = RouteRequestBuilderJ()
.start(8.681495, 49.41461)
.end(8.686507, 49.41943)
.language("en")
.build()
val response = client.getRoute(Profile.DRIVING_CAR, req)
assertNotNull(response)
require(response.routes.isNotEmpty())
}
}

View File

@@ -0,0 +1,47 @@
package org.nitri.ors
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.snap.SnapRequestBuilderJ
import org.nitri.ors.domain.snap.snapRequest
@RunWith(AndroidJUnit4::class)
class SnapDslInstrumentedTest {
@Test
fun dsl_buildsSnapRequest_andValidates() {
val req = snapRequest {
location(8.681495, 49.41461)
radius(50)
id = "snap-1"
}
assertEquals(1, req.locations.size)
assertEquals(50, req.radius)
assertEquals("snap-1", req.id)
}
@Test
fun dsl_requires_location_and_radius() {
assertThrows(IllegalArgumentException::class.java) {
snapRequest { radius(10) }
}
assertThrows(IllegalArgumentException::class.java) {
snapRequest { location(8.0, 48.0) }
}
}
@Test
fun javaBuilder_buildsSnapRequest_andValidates() {
val req = SnapRequestBuilderJ()
.location(8.0, 48.0)
.radius(25)
.id("jid")
.build()
assertEquals(1, req.locations.size)
assertEquals(25, req.radius)
assertEquals("jid", req.id)
}
}

View File

@@ -9,37 +9,39 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.repository.SnapRepository
import org.nitri.ors.helper.SnapHelper
@RunWith(AndroidJUnit4::class)
class SnapInstrumentedTest {
private fun createRepository(context: Context): SnapRepository {
private fun create(context: Context): Pair<DefaultOrsClient, SnapHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val api = OpenRouteServiceClient.create(apiKey, context)
return SnapRepository(api)
val client = DefaultOrsClient(apiKey, context)
val helper = SnapHelper()
return client to helper
}
@Test
fun testSnap_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, helper) = create(context)
// Two locations in/near Heidelberg, Germany [lon, lat]
val locations = listOf(
listOf(8.681495, 49.41461), // Heidelberg center
listOf(8.687872, 49.420318) // Nearby point
)
val profile = "driving-car"
val profile = Profile.DRIVING_CAR
val radius = 50 // meters
val response = repository.getSnap(
locations = locations,
radius = radius,
profile = profile,
id = "snap_test"
)
val response = with(helper) {
client.getSnap(
locations = locations,
radius = radius,
profile = profile,
id = "snap_test"
)
}
assertNotNull("Snap response should not be null", response)
assertNotNull("Metadata should be present", response.metadata)
@@ -55,21 +57,23 @@ class SnapInstrumentedTest {
@Test
fun testSnapJson_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, helper) = create(context)
val locations = listOf(
listOf(8.681495, 49.41461),
listOf(8.687872, 49.420318)
)
val profile = "driving-car"
val profile = Profile.DRIVING_CAR
val radius = 50
val response = repository.getSnapJson(
locations = locations,
radius = radius,
profile = profile,
id = "snap_json_test"
)
val response = with(helper) {
client.getSnapJson(
locations = locations,
radius = radius,
profile = profile,
id = "snap_json_test"
)
}
assertNotNull("Snap JSON response should not be null", response)
assertNotNull("Metadata should be present", response.metadata)
@@ -83,21 +87,23 @@ class SnapInstrumentedTest {
@Test
fun testSnapGeoJson_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val repository = createRepository(context)
val (client, repository) = create(context)
val locations = listOf(
listOf(8.681495, 49.41461),
listOf(8.687872, 49.420318)
)
val profile = "driving-car"
val profile = Profile.DRIVING_CAR
val radius = 50
val response = repository.getSnapGeoJson(
locations = locations,
radius = radius,
profile = profile,
id = "snap_geojson_test"
)
val response = with(repository) {
client.getSnapGeoJson(
locations = locations,
radius = radius,
profile = profile,
id = "snap_geojson_test"
)
}
assertNotNull("Snap GeoJSON response should not be null", response)
assertEquals("GeoJSON type should be FeatureCollection", "FeatureCollection", response.type)

View File

@@ -0,0 +1,300 @@
package org.nitri.ors
import android.content.Context
import org.nitri.ors.domain.elevation.ElevationLineRequest
import org.nitri.ors.domain.elevation.ElevationLineResponse
import org.nitri.ors.domain.elevation.ElevationPointRequest
import org.nitri.ors.domain.elevation.ElevationPointResponse
import org.nitri.ors.domain.export.ExportRequest
import org.nitri.ors.domain.export.ExportResponse
import org.nitri.ors.domain.export.TopoJsonExportResponse
import org.nitri.ors.domain.geocode.GeocodeSearchResponse
import org.nitri.ors.domain.isochrones.IsochronesRequest
import org.nitri.ors.domain.isochrones.IsochronesResponse
import org.nitri.ors.domain.matrix.MatrixRequest
import org.nitri.ors.domain.matrix.MatrixResponse
import org.nitri.ors.domain.optimization.OptimizationRequest
import org.nitri.ors.domain.optimization.OptimizationResponse
import org.nitri.ors.domain.pois.PoisGeoJsonResponse
import org.nitri.ors.domain.pois.PoisRequest
import org.nitri.ors.domain.route.GeoJsonRouteResponse
import org.nitri.ors.domain.route.RouteRequest
import org.nitri.ors.domain.route.RouteResponse
import org.nitri.ors.domain.snap.SnapGeoJsonResponse
import org.nitri.ors.domain.snap.SnapRequest
import org.nitri.ors.domain.snap.SnapResponse
import org.nitri.ors.restclient.OpenRouteServiceRestClient
/**
* Default implementation of [OrsClient] using the Retrofit based
* [OpenRouteServiceRestClient].
*/
class DefaultOrsClient(apiKey: String, context: Context) : OrsClient {
private val api = OpenRouteServiceRestClient.create(apiKey, context)
/** @inheritDoc */
override suspend fun getRoute(
profile: Profile,
routeRequest: RouteRequest
): RouteResponse {
return api.getRoute(profile.key, routeRequest)
}
/** @inheritDoc */
override suspend fun getRouteGpx(
profile: Profile,
routeRequest: RouteRequest
): String {
return api.getRouteGpx(profile.key, routeRequest).body()?.string() ?: ""
}
/** @inheritDoc */
override suspend fun getRouteGeoJson(
profile: Profile,
routeRequest: RouteRequest
): GeoJsonRouteResponse {
return api.getRouteGeoJson(profile.key, routeRequest)
}
/** @inheritDoc */
override suspend fun export(
profile: Profile,
exportRequest: ExportRequest
): ExportResponse {
return api.export(profile.key, exportRequest)
}
/** @inheritDoc */
override suspend fun exportJson(
profile: Profile,
exportRequest: ExportRequest
): ExportResponse {
return api.exportJson(profile.key, exportRequest)
}
/** @inheritDoc */
override suspend fun exportTopoJson(
profile: Profile,
exportRequest: ExportRequest
): TopoJsonExportResponse {
return api.exportTopoJson(profile.key, exportRequest)
}
/** @inheritDoc */
override suspend fun getIsochrones(
profile: Profile,
isochronesRequest: IsochronesRequest
): IsochronesResponse {
return api.getIsochrones(profile.key, isochronesRequest)
}
/** @inheritDoc */
override suspend fun getMatrix(
profile: Profile,
matrixRequest: MatrixRequest
): MatrixResponse {
return api.getMatrix(profile.key, matrixRequest)
}
/** @inheritDoc */
override suspend fun getSnap(
profile: Profile,
snapRequest: SnapRequest
): SnapResponse {
return api.getSnap(profile.key, snapRequest)
}
/** @inheritDoc */
override suspend fun getSnapJson(
profile: Profile,
snapRequest: SnapRequest
): SnapResponse {
return api.getSnapJson(profile.key, snapRequest)
}
/** @inheritDoc */
override suspend fun getSnapGeoJson(
profile: Profile,
snapRequest: SnapRequest
): SnapGeoJsonResponse {
return api.getSnapGeoJson(profile.key, snapRequest)
}
/** @inheritDoc */
override suspend fun getPois(poisRequest: PoisRequest): PoisGeoJsonResponse {
val raw = api.getPois(poisRequest)
fun PoisGeoJsonResponse.sanitized(): PoisGeoJsonResponse =
copy(bbox = bbox?.takeIf { it.size == 4 && it.all(Double::isFinite) })
return raw.sanitized()
}
/** @inheritDoc */
override suspend fun getOptimization(optimizationRequest: OptimizationRequest): OptimizationResponse {
return api.getOptimization(optimizationRequest)
}
/** @inheritDoc */
override suspend fun getElevationLine(
elevationLineRequest: ElevationLineRequest
): ElevationLineResponse {
return api.getElevationLine(elevationLineRequest)
}
/** @inheritDoc */
override suspend fun getElevationPoint(
elevationPointRequest: ElevationPointRequest
): ElevationPointResponse {
return api.getElevationPoint(elevationPointRequest)
}
/** @inheritDoc */
override suspend fun geocodeSearch(
text: String,
apiKey: String,
focusLon: Double?,
focusLat: Double?,
rectMinLon: Double?,
rectMinLat: Double?,
rectMaxLon: Double?,
rectMaxLat: Double?,
circleLon: Double?,
circleLat: Double?,
circleRadiusMeters: Double?,
boundaryGid: String?,
boundaryCountry: String?,
sourcesCsv: String?,
layersCsv: String?,
size: Int?
): GeocodeSearchResponse {
return api.geocodeSearch(
text = text,
focusLon = focusLon,
focusLat = focusLat,
rectMinLon = rectMinLon,
rectMinLat = rectMinLat,
rectMaxLon = rectMaxLon,
rectMaxLat = rectMaxLat,
circleLon = circleLon,
circleLat = circleLat,
circleRadiusMeters = circleRadiusMeters,
boundaryGid = boundaryGid,
boundaryCountry = boundaryCountry,
sourcesCsv = sourcesCsv,
layersCsv = layersCsv,
size = size,
apiKey = apiKey
)
}
/** @inheritDoc */
override suspend fun geocodeAutocomplete(
apiKey: String,
text: String,
focusLon: Double?,
focusLat: Double?,
rectMinLon: Double?,
rectMinLat: Double?,
rectMaxLon: Double?,
rectMaxLat: Double?,
circleLon: Double?,
circleLat: Double?,
circleRadius: Double?,
country: String?,
sources: List<String>?,
layers: List<String>?,
size: Int?
): GeocodeSearchResponse {
return api.autocomplete(
apiKey = apiKey,
text = text,
focusLon = focusLon,
focusLat = focusLat,
rectMinLon = rectMinLon,
rectMinLat = rectMinLat,
rectMaxLon = rectMaxLon,
rectMaxLat = rectMaxLat,
circleLon = circleLon,
circleLat = circleLat,
circleRadius = circleRadius,
country = country,
sources = sources,
layers = layers,
size = size
)
}
/** @inheritDoc */
override suspend fun geocodeStructured(
apiKey: String,
address: String?,
neighbourhood: String?,
borough: String?,
locality: String?,
county: String?,
region: String?,
country: String?,
postalcode: String?,
focusLon: Double?,
focusLat: Double?,
rectMinLon: Double?,
rectMinLat: Double?,
rectMaxLon: Double?,
rectMaxLat: Double?,
circleLon: Double?,
circleLat: Double?,
circleRadiusMeters: Double?,
boundaryCountry: String?,
layers: List<String>?,
sources: List<String>?,
size: Int?
): GeocodeSearchResponse {
return api.geocodeStructured(
apiKey = apiKey,
address = address,
neighbourhood = neighbourhood,
borough = borough,
locality = locality,
county = county,
region = region,
country = country,
postalcode = postalcode,
focusLon = focusLon,
focusLat = focusLat,
rectMinLon = rectMinLon,
rectMinLat = rectMinLat,
rectMaxLon = rectMaxLon,
rectMaxLat = rectMaxLat,
circleLon = circleLon,
circleLat = circleLat,
circleRadiusMeters = circleRadiusMeters,
boundaryCountry = boundaryCountry,
layers = layers,
sources = sources,
size = size
)
}
/** @inheritDoc */
override suspend fun geocodeReverse(
apiKey: String,
lon: Double,
lat: Double,
radiusKm: Double?,
size: Int?,
layers: List<String>?,
sources: List<String>?,
boundaryCountry: String?
): GeocodeSearchResponse {
return api.geocodeReverse(
apiKey = apiKey,
lon = lon,
lat = lat,
radiusKm = radiusKm,
size = size,
layers = layers,
sources = sources,
boundaryCountry = boundaryCountry
)
}
}

View File

@@ -1,10 +0,0 @@
package org.nitri.ors
import android.content.Context
import org.nitri.ors.client.OpenRouteServiceClient
import org.nitri.ors.repository.RouteRepository
class OpenRouteService(apiKey: String, context: Context) {
private val api = OpenRouteServiceClient.create(apiKey, context)
val routeRepository = RouteRepository(api)
}

View File

@@ -0,0 +1,17 @@
package org.nitri.ors
import android.content.Context
/**
* Factory for obtaining [OrsClient] instances.
*/
object Ors {
/**
* Creates a default [OrsClient] backed by Retrofit.
*
* @param apiKey API key obtained from openrouteservice.org
* @param context Android context used for HTTP client construction
*/
@JvmStatic
fun create(apiKey: String, context: Context): OrsClient = DefaultOrsClient(apiKey, context)
}

View File

@@ -0,0 +1,196 @@
package org.nitri.ors
import org.nitri.ors.domain.elevation.ElevationLineRequest
import org.nitri.ors.domain.elevation.ElevationLineResponse
import org.nitri.ors.domain.elevation.ElevationPointRequest
import org.nitri.ors.domain.elevation.ElevationPointResponse
import org.nitri.ors.domain.export.ExportRequest
import org.nitri.ors.domain.export.ExportResponse
import org.nitri.ors.domain.export.TopoJsonExportResponse
import org.nitri.ors.domain.geocode.GeocodeSearchResponse
import org.nitri.ors.domain.isochrones.IsochronesRequest
import org.nitri.ors.domain.isochrones.IsochronesResponse
import org.nitri.ors.domain.matrix.MatrixRequest
import org.nitri.ors.domain.matrix.MatrixResponse
import org.nitri.ors.domain.optimization.OptimizationRequest
import org.nitri.ors.domain.optimization.OptimizationResponse
import org.nitri.ors.domain.pois.PoisGeoJsonResponse
import org.nitri.ors.domain.pois.PoisRequest
import org.nitri.ors.domain.route.GeoJsonRouteResponse
import org.nitri.ors.domain.route.RouteRequest
import org.nitri.ors.domain.route.RouteResponse
import org.nitri.ors.domain.snap.SnapGeoJsonResponse
import org.nitri.ors.domain.snap.SnapRequest
import org.nitri.ors.domain.snap.SnapResponse
/**
* Supported routing profiles used across the OpenRouteService endpoints.
*
* The [key] corresponds to the profile identifier expected by the HTTP API.
*/
enum class Profile(val key: String) {
DRIVING_CAR("driving-car"),
DRIVING_HGV("driving-hgv"),
CYCLING_REGULAR("cycling-regular"),
CYCLING_ROAD("cycling-road"),
CYCLING_MOUNTAIN("cycling-mountain"),
CYCLING_ELECTRIC("cycling-electric"),
FOOT_WALKING("foot-walking"),
FOOT_HIKING("foot-hiking"),
WHEELCHAIR("wheelchair")
}
/** Longitude value wrapper used by the DSL builders. */
@JvmInline
value class Lon(val v: Double)
/** Latitude value wrapper used by the DSL builders. */
@JvmInline
value class Lat(val v: Double)
/** Convenience pair for longitude/latitude coordinates. */
data class LonLat(val lon: Double, val lat: Double)
/**
* Abstraction over the OpenRouteService HTTP API.
*
* Implementations map these suspending functions to the respective REST
* endpoints as documented in the
* [OpenRouteService API reference](https://openrouteservice.org/dev/#/api-docs).
*/
interface OrsClient {
// Directions
/**
* Requests a route using the Directions endpoint.
* `GET /v2/directions/{profile}`
*/
suspend fun getRoute(profile: Profile, routeRequest: RouteRequest): RouteResponse
/** Retrieves the route as GPX. */
suspend fun getRouteGpx(profile: Profile, routeRequest: RouteRequest): String
/** Retrieves the route as GeoJSON feature collection. */
suspend fun getRouteGeoJson(profile: Profile, routeRequest: RouteRequest): GeoJsonRouteResponse
// Export
/** Calls the export endpoint returning plain JSON. */
suspend fun export(profile: Profile, exportRequest: ExportRequest): ExportResponse
/** Same as [export] but explicitly requesting JSON output. */
suspend fun exportJson(profile: Profile, exportRequest: ExportRequest): ExportResponse
/** Requests TopoJSON output from the export endpoint. */
suspend fun exportTopoJson(profile: Profile, exportRequest: ExportRequest): TopoJsonExportResponse
// Isochrones
/** Accesses the isochrones endpoint. */
suspend fun getIsochrones(profile: Profile, isochronesRequest: IsochronesRequest): IsochronesResponse
// Matrix
/** Calls the matrix endpoint and returns distance/duration matrices. */
suspend fun getMatrix(profile: Profile, matrixRequest: MatrixRequest): MatrixResponse
// Snapping
/** Snaps coordinates to the road network. */
suspend fun getSnap(profile: Profile, snapRequest: SnapRequest): SnapResponse
/** Snaps coordinates and returns the JSON variant of the response. */
suspend fun getSnapJson(profile: Profile, snapRequest: SnapRequest): SnapResponse
/** Snaps coordinates and returns a GeoJSON response. */
suspend fun getSnapGeoJson(profile: Profile, snapRequest: SnapRequest): SnapGeoJsonResponse
// POIs
/** Queries points of interest and returns a GeoJSON feature collection. */
suspend fun getPois(poisRequest: PoisRequest): PoisGeoJsonResponse
// Optimization
/** Delegates to the VROOM based optimization service. */
suspend fun getOptimization(optimizationRequest: OptimizationRequest): OptimizationResponse
// Elevation
/** Calls the elevation/line endpoint. */
suspend fun getElevationLine(elevationLineRequest: ElevationLineRequest): ElevationLineResponse
/** Calls the elevation/point endpoint. */
suspend fun getElevationPoint(elevationPointRequest: ElevationPointRequest): ElevationPointResponse
// Geocode
/** Forward geocoding search endpoint. */
suspend fun geocodeSearch(
text: String,
apiKey: String,
focusLon: Double? = null,
focusLat: Double? = null,
rectMinLon: Double? = null,
rectMinLat: Double? = null,
rectMaxLon: Double? = null,
rectMaxLat: Double? = null,
circleLon: Double? = null,
circleLat: Double? = null,
circleRadiusMeters: Double? = null,
boundaryGid: String? = null,
boundaryCountry: String? = null,
sourcesCsv: String? = null,
layersCsv: String? = null,
size: Int? = 10,
): GeocodeSearchResponse
/** Autocomplete endpoint returning suggestions for a partial query. */
suspend fun geocodeAutocomplete(
apiKey: String,
text: String,
focusLon: Double? = null,
focusLat: Double? = null,
rectMinLon: Double? = null,
rectMinLat: Double? = null,
rectMaxLon: Double? = null,
rectMaxLat: Double? = null,
circleLon: Double? = null,
circleLat: Double? = null,
circleRadius: Double? = null,
country: String? = null,
sources: List<String>? = null,
layers: List<String>? = null,
size: Int? = null,
): GeocodeSearchResponse
/** Structured forward geocoding using discrete address fields. */
suspend fun geocodeStructured(
apiKey: String,
address: String? = null,
neighbourhood: String? = null,
borough: String? = null,
locality: String? = null,
county: String? = null,
region: String? = null,
country: String? = null,
postalcode: String? = null,
focusLon: Double? = null,
focusLat: Double? = null,
rectMinLon: Double? = null,
rectMinLat: Double? = null,
rectMaxLon: Double? = null,
rectMaxLat: Double? = null,
circleLon: Double? = null,
circleLat: Double? = null,
circleRadiusMeters: Double? = null,
boundaryCountry: String? = null,
layers: List<String>? = null,
sources: List<String>? = null,
size: Int? = null,
): GeocodeSearchResponse
/** Reverse geocoding for a single coordinate. */
suspend fun geocodeReverse(
apiKey: String,
lon: Double,
lat: Double,
radiusKm: Double? = null,
size: Int? = null,
layers: List<String>? = null,
sources: List<String>? = null,
boundaryCountry: String? = null,
): GeocodeSearchResponse
}

View File

@@ -1,28 +1,28 @@
package org.nitri.ors.api
import okhttp3.ResponseBody
import org.nitri.ors.model.elevation.ElevationLineRequest
import org.nitri.ors.model.elevation.ElevationLineResponse
import org.nitri.ors.model.elevation.ElevationPointRequest
import org.nitri.ors.model.elevation.ElevationPointResponse
import org.nitri.ors.model.export.ExportRequest
import org.nitri.ors.model.export.ExportResponse
import org.nitri.ors.model.export.TopoJsonExportResponse
import org.nitri.ors.model.geocode.GeocodeSearchResponse
import org.nitri.ors.model.isochrones.IsochronesRequest
import org.nitri.ors.model.isochrones.IsochronesResponse
import org.nitri.ors.model.matrix.MatrixRequest
import org.nitri.ors.model.matrix.MatrixResponse
import org.nitri.ors.model.optimization.OptimizationRequest
import org.nitri.ors.model.optimization.OptimizationResponse
import org.nitri.ors.model.pois.PoisGeoJsonResponse
import org.nitri.ors.model.pois.PoisRequest
import org.nitri.ors.model.route.GeoJsonRouteResponse
import org.nitri.ors.model.route.RouteRequest
import org.nitri.ors.model.route.RouteResponse
import org.nitri.ors.model.snap.SnapGeoJsonResponse
import org.nitri.ors.model.snap.SnapRequest
import org.nitri.ors.model.snap.SnapResponse
import org.nitri.ors.domain.elevation.ElevationLineRequest
import org.nitri.ors.domain.elevation.ElevationLineResponse
import org.nitri.ors.domain.elevation.ElevationPointRequest
import org.nitri.ors.domain.elevation.ElevationPointResponse
import org.nitri.ors.domain.export.ExportRequest
import org.nitri.ors.domain.export.ExportResponse
import org.nitri.ors.domain.export.TopoJsonExportResponse
import org.nitri.ors.domain.geocode.GeocodeSearchResponse
import org.nitri.ors.domain.isochrones.IsochronesRequest
import org.nitri.ors.domain.isochrones.IsochronesResponse
import org.nitri.ors.domain.matrix.MatrixRequest
import org.nitri.ors.domain.matrix.MatrixResponse
import org.nitri.ors.domain.optimization.OptimizationRequest
import org.nitri.ors.domain.optimization.OptimizationResponse
import org.nitri.ors.domain.pois.PoisGeoJsonResponse
import org.nitri.ors.domain.pois.PoisRequest
import org.nitri.ors.domain.route.GeoJsonRouteResponse
import org.nitri.ors.domain.route.RouteRequest
import org.nitri.ors.domain.route.RouteResponse
import org.nitri.ors.domain.snap.SnapGeoJsonResponse
import org.nitri.ors.domain.snap.SnapRequest
import org.nitri.ors.domain.snap.SnapResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
@@ -33,7 +33,6 @@ import retrofit2.http.Query
interface OpenRouteServiceApi {
// Directions
@GET("v2/directions/{profile}")
suspend fun getRouteSimple(
@Path("profile") profile: String,

View File

@@ -0,0 +1,97 @@
package org.nitri.ors.domain.elevation
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
/** DSL for building [ElevationLineRequest] instances. */
inline fun elevationLineRequest(build: ElevationLineRequestBuilder.() -> Unit): ElevationLineRequest =
ElevationLineRequestBuilder().apply(build).build()
/** Builder used by [elevationLineRequest]. */
class ElevationLineRequestBuilder {
var formatIn: String = ElevationFormats.POLYLINE
var formatOut: String = ElevationFormats.GEOJSON
var dataset: String? = null
// flexible geometry holder; convenience helpers below
private var geometry: JsonElement? = null
fun geometry(element: JsonElement) = apply { this.geometry = element }
// Convenience: provide a simple list of [lon,lat] pairs for polyline format
fun polyline(vararg coords: Pair<Double, Double>) = apply {
formatIn = ElevationFormats.POLYLINE
geometry = buildJsonArray {
coords.forEach { (lon, lat) ->
add(buildJsonArray { add(JsonPrimitive(lon)); add(JsonPrimitive(lat)) })
}
}
}
fun build(): ElevationLineRequest {
val g = requireNotNull(geometry) { "geometry is required" }
return ElevationLineRequest(
formatIn = formatIn,
formatOut = formatOut,
geometry = g,
dataset = dataset
)
}
}
/** DSL for constructing [ElevationPointRequest] objects. */
inline fun elevationPointRequest(build: ElevationPointRequestBuilder.() -> Unit): ElevationPointRequest =
ElevationPointRequestBuilder().apply(build).build()
/** Builder used by [elevationPointRequest]. */
class ElevationPointRequestBuilder {
var formatIn: String = "point"
var formatOut: String = "geojson"
var dataset: String? = null
private var lon: Double? = null
private var lat: Double? = null
fun point(lon: Double, lat: Double) = apply { this.lon = lon; this.lat = lat }
fun build(): ElevationPointRequest {
val lo = requireNotNull(lon) { "lon is required" }
val la = requireNotNull(lat) { "lat is required" }
return ElevationPointRequest(
formatIn = formatIn,
formatOut = formatOut,
dataset = dataset,
geometry = listOf(lo, la)
)
}
}
// Java-friendly builders
class ElevationLineRequestBuilderJ {
private val dsl = ElevationLineRequestBuilder()
fun formatIn(v: String) = apply { dsl.formatIn = v }
fun formatOut(v: String) = apply { dsl.formatOut = v }
fun dataset(v: String?) = apply { dsl.dataset = v }
fun geometry(element: JsonElement) = apply { dsl.geometry(element) }
fun polyline(coords: List<List<Double>>) = apply {
// Accept list of [lon,lat]; convert to JsonArray
val arr: JsonArray = buildJsonArray {
coords.forEach { c ->
add(buildJsonArray { add(JsonPrimitive(c[0])); add(JsonPrimitive(c[1])) })
}
}
dsl.formatIn = ElevationFormats.POLYLINE
dsl.geometry(arr)
}
fun build(): ElevationLineRequest = dsl.build()
}
class ElevationPointRequestBuilderJ {
private val dsl = ElevationPointRequestBuilder()
fun formatIn(v: String) = apply { dsl.formatIn = v }
fun formatOut(v: String) = apply { dsl.formatOut = v }
fun dataset(v: String?) = apply { dsl.dataset = v }
fun point(lon: Double, lat: Double) = apply { dsl.point(lon, lat) }
fun build(): ElevationPointRequest = dsl.build()
}

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.elevation
package org.nitri.ors.domain.elevation
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.elevation
package org.nitri.ors.domain.elevation
import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,22 @@
package org.nitri.ors.domain.elevation
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/** Request for the `elevation/point` endpoint. */
@Serializable
data class ElevationPointRequest(
@SerialName("format_in")
/** Input format, e.g. `point`. */
val formatIn: String,
@SerialName("format_out")
/** Output format: `geojson` or `point`. */
val formatOut: String = "geojson",
/** Optional elevation dataset name, e.g. `srtm`. */
val dataset: String? = null,
/** Coordinate as `[lon, lat]`. */
val geometry: List<Double>
)

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.elevation
package org.nitri.ors.domain.elevation
import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,45 @@
package org.nitri.ors.domain.export
/** DSL for creating [ExportRequest] objects. */
inline fun exportRequest(build: ExportRequestBuilder.() -> Unit): ExportRequest =
ExportRequestBuilder().apply(build).build()
/** Builder used by [exportRequest]. */
class ExportRequestBuilder {
private var bbox: List<List<Double>>? = null
private var id: String? = null
private var geometry: Boolean? = null
fun bbox(minLon: Double, minLat: Double, maxLon: Double, maxLat: Double) = apply {
bbox = listOf(listOf(minLon, minLat), listOf(maxLon, maxLat))
}
fun id(id: String) = apply { this.id = id }
fun geometry(geometry: Boolean?) = apply { this.geometry = geometry }
fun build(): ExportRequest {
val b = requireNotNull(bbox) { "bbox is required" }
val i = id ?: "export_request"
return ExportRequest(bbox = b, id = i, geometry = geometry)
}
}
/** Java-friendly builder counterpart. */
class ExportRequestBuilderJ {
private var bbox: List<List<Double>>? = null
private var id: String? = null
private var geometry: Boolean? = null
fun bbox(minLon: Double, minLat: Double, maxLon: Double, maxLat: Double) = apply {
bbox = listOf(listOf(minLon, minLat), listOf(maxLon, maxLat))
}
fun id(id: String) = apply { this.id = id }
fun geometry(geometry: Boolean?) = apply { this.geometry = geometry }
fun build(): ExportRequest {
val b = requireNotNull(bbox) { "bbox is required" }
val i = id ?: "export_request"
return ExportRequest(bbox = b, id = i, geometry = geometry)
}
}

View File

@@ -0,0 +1,14 @@
package org.nitri.ors.domain.export
import kotlinx.serialization.Serializable
/** Request payload for the export endpoint. */
@Serializable
data class ExportRequest(
/** Bounding box specified as `[[minLon,minLat],[maxLon,maxLat]]`. */
val bbox: List<List<Double>>,
/** Client-specified identifier. */
val id: String,
/** Whether to include full geometry in the response. */
val geometry: Boolean? = null
)

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.export
package org.nitri.ors.domain.export
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
@@ -12,23 +12,26 @@ import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.doubleOrNull
/** Graph export response containing nodes and edges. */
@Serializable
data class ExportResponse(
val nodes: List<Node> = emptyList(),
val edges: List<GraphEdge> = emptyList()
)
/** Node entry in an [ExportResponse]. */
@Serializable
data class Node(
@SerialName("nodeId") val nodeId: Long,
/** [lon, lat] */
/** `[lon, lat]` coordinate. */
val location: List<Double>
)
/** Graph edge entry in an [ExportResponse]. */
@Serializable
data class GraphEdge(
@SerialName("fromId") val fromId: Long,
@SerialName("toId") val toId: Long,
@SerialName("toId") val toId: Long,
@Serializable(with = StringAsDoubleSerializer::class)
val weight: Double
)

View File

@@ -0,0 +1,42 @@
package org.nitri.ors.domain.export
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/** TopoJSON variant of the export response. */
@Serializable
data class TopoJsonExportResponse(
val type: String,
val objects: TopoObjects,
/** Top-level arcs represented as `[lon, lat]` coordinate pairs. */
val arcs: List<List<List<Double>>>,
val bbox: List<Double>
)
@Serializable
data class TopoObjects(
val network: GeometryCollection
)
@Serializable
data class GeometryCollection(
val type: String,
val geometries: List<TopoGeometry>
)
@Serializable
data class TopoGeometry(
val type: String,
/** Indices into the top-level [TopoJsonExportResponse.arcs] array. */
val arcs: List<Int>,
val properties: GeometryProps? = null
)
@Serializable
data class GeometryProps(
/** Edge weight often supplied as a string. */
val weight: String? = null,
@SerialName("node_from") val nodeFrom: Long? = null,
@SerialName("node_to") val nodeTo: Long? = null
)

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.geocode
package org.nitri.ors.domain.geocode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,84 @@
package org.nitri.ors.domain.isochrones
/** DSL for constructing [IsochronesRequest] objects. */
inline fun isochronesRequest(build: IsochronesRequestBuilder.() -> Unit): IsochronesRequest =
IsochronesRequestBuilder().apply(build).build()
/** Builder used by [isochronesRequest]. */
class IsochronesRequestBuilder {
private val locations = mutableListOf<List<Double>>()
private var range: MutableList<Int> = mutableListOf()
var rangeType: String? = null
var attributes: List<String>? = null
var id: String? = null
var intersections: Boolean? = null
var interval: Int? = null
var locationType: String? = null
var smoothing: Double? = null
var options: IsochronesOptions? = null
fun location(lon: Double, lat: Double) = apply { locations.add(listOf(lon, lat)) }
fun rangeSeconds(vararg seconds: Int) = apply { range.addAll(seconds.toList()) }
fun build(): IsochronesRequest {
require(locations.isNotEmpty()) { "At least one location is required" }
require(range.isNotEmpty()) { "At least one range value is required" }
return IsochronesRequest(
locations = locations.toList(),
range = range.toList(),
rangeType = rangeType,
attributes = attributes,
id = id,
intersections = intersections,
interval = interval,
locationType = locationType,
smoothing = smoothing,
options = options
)
}
}
/** Java-friendly builder counterpart. */
class IsochronesRequestBuilderJ {
private val locations = mutableListOf<List<Double>>()
private val range: MutableList<Int> = mutableListOf()
private var rangeType: String? = null
private var attributes: List<String>? = null
private var id: String? = null
private var intersections: Boolean? = null
private var interval: Int? = null
private var locationType: String? = null
private var smoothing: Double? = null
private var options: IsochronesOptions? = null
fun location(lon: Double, lat: Double) = apply { locations.add(listOf(lon, lat)) }
fun addRange(seconds: Int) = apply { range.add(seconds) }
fun rangeType(rangeType: String?) = apply { this.rangeType = rangeType }
fun attributes(attributes: List<String>?) = apply { this.attributes = attributes }
fun id(id: String?) = apply { this.id = id }
fun intersections(intersections: Boolean?) = apply { this.intersections = intersections }
fun interval(interval: Int?) = apply { this.interval = interval }
fun locationType(locationType: String?) = apply { this.locationType = locationType }
fun smoothing(smoothing: Double?) = apply { this.smoothing = smoothing }
fun options(options: IsochronesOptions?) = apply { this.options = options }
fun build(): IsochronesRequest {
require(locations.isNotEmpty()) { "At least one location is required" }
require(range.isNotEmpty()) { "At least one range value is required" }
return IsochronesRequest(
locations = locations.toList(),
range = range.toList(),
rangeType = rangeType,
attributes = attributes,
id = id,
intersections = intersections,
interval = interval,
locationType = locationType,
smoothing = smoothing,
options = options
)
}
}

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.isochrones
package org.nitri.ors.domain.isochrones
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -1,8 +1,9 @@
package org.nitri.ors.model.isochrones
package org.nitri.ors.domain.isochrones
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/** GeoJSON response returned by the isochrones endpoint. */
@Serializable
data class IsochronesResponse(
val type: String,
@@ -11,6 +12,7 @@ data class IsochronesResponse(
val metadata: IsochronesMetadata
)
/** Individual isochrone feature. */
@Serializable
data class IsochroneFeature(
val type: String,
@@ -25,12 +27,11 @@ data class IsochroneProperties(
val center: List<Double>
)
/** Geometry of an isochrone feature. */
@Serializable
data class IsochroneGeometry(
val type: String, // "Polygon" (sometimes "MultiPolygon")
// ORS is returning Polygon for your case:
val type: String,
val coordinates: List<List<List<Double>>>
// If you later see MultiPolygon, change to List<List<List<List<Double>>>> or make it polymorphic.
)
@Serializable

View File

@@ -0,0 +1,58 @@
package org.nitri.ors.domain.matrix
/** DSL for building [MatrixRequest] instances. */
inline fun matrixRequest(build: MatrixRequestBuilder.() -> Unit): MatrixRequest =
MatrixRequestBuilder().apply(build).build()
/** Builder used by [matrixRequest]. */
class MatrixRequestBuilder {
private val locations = mutableListOf<List<Double>>()
var destinations: List<Int>? = null
var id: String? = null
var metrics: List<String>? = null
var resolveLocations: Boolean? = null
var sources: List<Int>? = null
fun location(lon: Double, lat: Double) = apply { locations.add(listOf(lon, lat)) }
fun build(): MatrixRequest {
require(locations.isNotEmpty()) { "At least one location is required" }
return MatrixRequest(
locations = locations.toList(),
destinations = destinations,
id = id,
metrics = metrics,
resolveLocations = resolveLocations,
sources = sources
)
}
}
/** Java-friendly builder counterpart. */
class MatrixRequestBuilderJ {
private val locations = mutableListOf<List<Double>>()
private var destinations: List<Int>? = null
private var id: String? = null
private var metrics: List<String>? = null
private var resolveLocations: Boolean? = null
private var sources: List<Int>? = null
fun location(lon: Double, lat: Double) = apply { locations.add(listOf(lon, lat)) }
fun destinations(destinations: List<Int>?) = apply { this.destinations = destinations }
fun id(id: String?) = apply { this.id = id }
fun metrics(metrics: List<String>?) = apply { this.metrics = metrics }
fun resolveLocations(resolveLocations: Boolean?) = apply { this.resolveLocations = resolveLocations }
fun sources(sources: List<Int>?) = apply { this.sources = sources }
fun build(): MatrixRequest {
require(locations.isNotEmpty()) { "At least one location is required" }
return MatrixRequest(
locations = locations.toList(),
destinations = destinations,
id = id,
metrics = metrics,
resolveLocations = resolveLocations,
sources = sources
)
}
}

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.matrix
package org.nitri.ors.domain.matrix
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -1,8 +1,8 @@
package org.nitri.ors.model.matrix
package org.nitri.ors.domain.matrix
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.nitri.ors.model.meta.Metadata
import org.nitri.ors.domain.meta.Metadata
/**
* Represents the response from an ORS Matrix API request.

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.meta
package org.nitri.ors.domain.meta
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,86 @@
package org.nitri.ors.domain.optimization
/** DSL for building [OptimizationRequest] payloads. */
inline fun optimizationRequest(build: OptimizationRequestBuilder.() -> Unit): OptimizationRequest =
OptimizationRequestBuilder().apply(build).build()
/** Builder used by [optimizationRequest]. */
class OptimizationRequestBuilder {
private val jobs = mutableListOf<Job>()
private val shipments = mutableListOf<Shipment>()
private val vehicles = mutableListOf<Vehicle>()
private var matrices: Map<String, CustomMatrix>? = null
private var options: Map<String, kotlinx.serialization.json.JsonElement>? = null
fun job(id: Int, locationLon: Double? = null, locationLat: Double? = null, service: Int? = null, priority: Int? = null) = apply {
val loc = if (locationLon != null && locationLat != null) listOf(locationLon, locationLat) else null
jobs.add(Job(id = id, service = service, priority = priority, location = loc))
}
fun shipment(
id: Int,
pickupLon: Double, pickupLat: Double,
deliveryLon: Double, deliveryLat: Double,
servicePickup: Int? = null,
serviceDelivery: Int? = null
) = apply {
val pickup = ShipmentStep(service = servicePickup, location = listOf(pickupLon, pickupLat))
val delivery = ShipmentStep(service = serviceDelivery, location = listOf(deliveryLon, deliveryLat))
shipments.add(Shipment(id = id, amount = null, pickup = pickup, delivery = delivery))
}
fun vehicle(id: Int, profile: String, startLon: Double? = null, startLat: Double? = null, endLon: Double? = null, endLat: Double? = null, capacity: List<Int>? = null) = apply {
val start = if (startLon != null && startLat != null) listOf(startLon, startLat) else null
val end = if (endLon != null && endLat != null) listOf(endLon, endLat) else null
vehicles.add(
Vehicle(
id = id,
profile = profile,
capacity = capacity,
start = start,
end = end
)
)
}
fun matrices(m: Map<String, CustomMatrix>?) = apply { this.matrices = m }
fun options(o: Map<String, kotlinx.serialization.json.JsonElement>?) = apply { this.options = o }
fun build(): OptimizationRequest {
if (vehicles.isEmpty()) {
throw IllegalStateException("At least one vehicle is required")
}
if (jobs.isEmpty() && shipments.isEmpty()) {
error("Provide at least one job or shipment")
}
return OptimizationRequest(
jobs = jobs.takeIf { it.isNotEmpty() },
shipments = shipments.takeIf { it.isNotEmpty() },
vehicles = vehicles.toList(),
matrices = matrices,
options = options
)
}
}
/** Java-friendly builder counterpart. */
class OptimizationRequestBuilderJ {
private val dsl = OptimizationRequestBuilder()
fun addJob(id: Int, locationLon: Double?, locationLat: Double?, service: Int?, priority: Int?) = apply {
dsl.job(id, locationLon, locationLat, service, priority)
}
fun addShipment(id: Int, pickupLon: Double, pickupLat: Double, deliveryLon: Double, deliveryLat: Double, servicePickup: Int?, serviceDelivery: Int?) = apply {
dsl.shipment(id, pickupLon, pickupLat, deliveryLon, deliveryLat, servicePickup, serviceDelivery)
}
fun addVehicle(id: Int, profile: String, startLon: Double?, startLat: Double?, endLon: Double?, endLat: Double?, capacity: List<Int>?) = apply {
dsl.vehicle(id, profile, startLon, startLat, endLon, endLat, capacity)
}
fun matrices(m: Map<String, CustomMatrix>?) = apply { dsl.matrices(m) }
fun options(o: Map<String, kotlinx.serialization.json.JsonElement>?) = apply { dsl.options(o) }
fun build(): OptimizationRequest = dsl.build()
}

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.optimization
package org.nitri.ors.domain.optimization
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.optimization
package org.nitri.ors.domain.optimization
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,79 @@
package org.nitri.ors.domain.pois
/** DSL for constructing [PoisRequest] objects. */
inline fun poisRequest(build: PoisRequestBuilder.() -> Unit): PoisRequest =
PoisRequestBuilder().apply(build).build()
/** Builder used by [poisRequest]. */
class PoisRequestBuilder {
private var bbox: List<List<Double>>? = null
private var geojson: GeoJsonGeometry? = null
private var buffer: Int? = null
var filters: Map<String, String>? = null
var limit: Int? = null
var sortby: String? = null
fun bbox(minLon: Double, minLat: Double, maxLon: Double, maxLat: Double) = apply {
bbox = listOf(listOf(minLon, minLat), listOf(maxLon, maxLat))
}
fun point(lon: Double, lat: Double) = apply {
geojson = GeoJsonGeometry(type = "Point", coordinates = listOf(lon, lat))
}
fun buffer(meters: Int?) = apply { this.buffer = meters }
fun build(): PoisRequest {
val geom = Geometry(
bbox = bbox,
geojson = geojson,
buffer = buffer
)
return PoisRequest(
geometry = geom,
filters = filters,
limit = limit,
sortby = sortby
)
}
}
/** Java-friendly builder counterpart. */
class PoisRequestBuilderJ {
private var bbox: List<List<Double>>? = null
private var geojson: GeoJsonGeometry? = null
private var buffer: Int? = null
private var filters: Map<String, String>? = null
private var limit: Int? = null
private var sortby: String? = null
fun bbox(minLon: Double, minLat: Double, maxLon: Double, maxLat: Double) = apply {
bbox = listOf(listOf(minLon, minLat), listOf(maxLon, maxLat))
}
fun point(lon: Double, lat: Double) = apply {
geojson = GeoJsonGeometry(type = "Point", coordinates = listOf(lon, lat))
}
fun buffer(meters: Int?) = apply { this.buffer = meters }
fun filters(filters: Map<String, String>?) = apply { this.filters = filters }
fun limit(limit: Int?) = apply { this.limit = limit }
fun sortby(sortby: String?) = apply { this.sortby = sortby }
fun build(): PoisRequest {
val geom = Geometry(
bbox = bbox,
geojson = geojson,
buffer = buffer
)
return PoisRequest(
geometry = geom,
filters = filters,
limit = limit,
sortby = sortby
)
}
}

View File

@@ -0,0 +1,31 @@
package org.nitri.ors.domain.pois
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable
/** Request for the POIs endpoint. */
@Serializable
data class PoisRequest(
@Required val request: String = "pois",
val geometry: Geometry,
val filters: Map<String, String>? = null,
val limit: Int? = null,
val sortby: String? = null
)
@Serializable
data class Geometry(
/** Bounding box `[[minLon,minLat],[maxLon,maxLat]]` if set. */
val bbox: List<List<Double>>? = null,
/** Optional GeoJSON geometry. */
val geojson: GeoJsonGeometry? = null,
/** Optional buffer in meters applied to the geometry. */
val buffer: Int? = null
)
@Serializable
data class GeoJsonGeometry(
val type: String,
/** `[lon, lat]` pair defining the point location. */
val coordinates: List<Double>
)

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.pois
package org.nitri.ors.domain.pois
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,23 @@
package org.nitri.ors.domain.route
import org.nitri.ors.LonLat
/** DSL for constructing [RouteRequest] instances. */
inline fun routeRequest(build: RouteRequestBuilder.() -> Unit): RouteRequest =
RouteRequestBuilder().apply(build).build()
/** Builder used by [routeRequest]. */
class RouteRequestBuilder {
private val coords = mutableListOf<LonLat>()
var language: String? = null
fun start(lon: Double, lat: Double) = apply { coords.add(LonLat(lon, lat)) }
fun end(lon: Double, lat: Double) = apply { coords.add(LonLat(lon, lat)) }
fun coordinate(lon: Double, lat: Double) = apply { coords.add(LonLat(lon, lat)) }
fun build(): RouteRequest {
require(coords.size >= 2) { "At least start and end coordinates required" }
val coordinates = coords.map { listOf(it.lon, it.lat) }
return RouteRequest(coordinates, language = language)
}
}

View File

@@ -1,8 +1,9 @@
package org.nitri.ors.model.route
package org.nitri.ors.domain.route
import kotlinx.serialization.Serializable
import org.nitri.ors.model.meta.Metadata
import org.nitri.ors.domain.meta.Metadata
/** GeoJSON response for the directions endpoint. */
@Serializable
data class GeoJsonRouteResponse(
val type: String,
@@ -11,6 +12,7 @@ data class GeoJsonRouteResponse(
val metadata: Metadata
)
/** A single GeoJSON feature within the route response. */
@Serializable
data class Feature(
val type: String,
@@ -18,6 +20,7 @@ data class Feature(
val properties: Map<String, kotlinx.serialization.json.JsonElement> = emptyMap()
)
/** Geometry of a route feature. */
@Serializable
data class Geometry(
val type: String,

View File

@@ -1,8 +1,9 @@
package org.nitri.ors.model.route
package org.nitri.ors.domain.route
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
/** GPX response returned by the directions endpoint when requesting GPX. */
@Serializable
data class GpxResponse(
val metadata: GpxMetadata,

View File

@@ -1,41 +1,67 @@
package org.nitri.ors.model.route
package org.nitri.ors.domain.route
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Payload for the directions `v2/directions/{profile}` endpoint.
*
* All parameters map to the official ORS API; nullable properties are omitted
* from the serialized JSON when not provided.
*/
@Serializable
data class RouteRequest(
/** List of `[lon, lat]` coordinate pairs in order of travel. */
val coordinates: List<List<Double>>,
/** Search radiuses around each coordinate in meters. */
val radiuses: List<Double>? = null,
/** List of `[bearing, range]` constraints for each coordinate. */
val bearings: List<List<Double>>? = null,
/** Include elevation data in response. */
val elevation: Boolean? = null,
@SerialName("extra_info")
/** Additional information to include, e.g., `waytype`. */
val extraInfo: List<String>? = null,
/** Whether to return turn-by-turn instructions. */
val instructions: Boolean? = null,
@SerialName("instructions_format")
/** Format of instructions such as `html` or `text`. */
val instructionsFormat: String? = null,
/** Preferred language for textual parts of the response. */
val language: String? = null,
/** Routing preference such as `fastest` or `shortest`. */
val preference: String? = null,
/** Unit system for distances. */
val units: String? = null,
/** Whether to include geometry. */
val geometry: Boolean? = null,
@SerialName("geometry_simplify")
/** Simplify the returned geometry. */
val geometrySimplify: Boolean? = null,
@SerialName("roundabout_exits")
/** Return `exit` indices for roundabouts. */
val roundaboutExits: Boolean? = null,
/** Additional attributes to include for each segment. */
val attributes: List<String>? = null,
/** Include a list of maneuvers. */
val maneuvers: Boolean? = null,
@SerialName("continue_straight")
/** Force continue straight at waypoints? */
val continueStraight: Boolean? = null,
/** Optional advanced options. */
val options: RouteOptions? = null
)
@Serializable
data class RouteOptions(
@SerialName("avoid_features")
/** Features to avoid, e.g., `ferries` or `tollways`. */
val avoidFeatures: List<String>? = null,
@SerialName("avoid_polygons")
/** Optional polygon geometry to avoid. */
val avoidPolygons: AvoidPolygons? = null,
@SerialName("profile_params")
/** Profile specific parameters such as restrictions. */
val profileParams: ProfileParams? = null
)

View File

@@ -0,0 +1,22 @@
package org.nitri.ors.domain.route
import org.nitri.ors.LonLat
/**
* Java-friendly builder
*/
class RouteRequestBuilderJ {
private val coords = mutableListOf<LonLat>()
private var language: String? = null
fun start(lon: Double, lat: Double) = apply { coords.add(LonLat(lon, lat)) }
fun end(lon: Double, lat: Double) = apply { coords.add(LonLat(lon, lat)) }
fun add(lon: Double, lat: Double) = apply { coords.add(LonLat(lon, lat)) }
fun language(language: String) = apply { this.language = language }
fun build(): RouteRequest {
require(coords.size >= 2) { "At least start and end coordinates required" }
val coordinates = coords.map { listOf(it.lon, it.lat) }
return RouteRequest(coordinates, language = language)
}
}

View File

@@ -1,9 +1,10 @@
package org.nitri.ors.model.route
package org.nitri.ors.domain.route
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.nitri.ors.model.meta.Metadata
import org.nitri.ors.domain.meta.Metadata
/** Response from the directions endpoint. */
@Serializable
data class RouteResponse(
val routes: List<Route>,
@@ -11,6 +12,7 @@ data class RouteResponse(
val metadata: Metadata? = null
)
/** Single route variant returned by the API. */
@Serializable
data class Route(
val summary: RouteSummary,
@@ -20,6 +22,7 @@ data class Route(
val wayPoints: List<Int>? = null
)
/** Aggregated summary of a route. */
@Serializable
data class RouteSummary(
val distance: Double,
@@ -28,6 +31,7 @@ data class RouteSummary(
val descent: Double? = null
)
/** One segment between intermediate waypoints. */
@Serializable
data class Segment(
val distance: Double,
@@ -40,6 +44,7 @@ data class Segment(
val percentage: Double? = null
)
/** Turn instruction within a segment. */
@Serializable
data class Step(
val distance: Double,

View File

@@ -0,0 +1,38 @@
package org.nitri.ors.domain.snap
/** DSL for constructing [SnapRequest] objects. */
inline fun snapRequest(build: SnapRequestBuilder.() -> Unit): SnapRequest =
SnapRequestBuilder().apply(build).build()
/** Builder used by [snapRequest]. */
class SnapRequestBuilder {
private val locations = mutableListOf<List<Double>>()
private var radius: Int? = null
var id: String? = null
fun location(lon: Double, lat: Double) = apply { locations.add(listOf(lon, lat)) }
fun radius(meters: Int) = apply { this.radius = meters }
fun build(): SnapRequest {
require(locations.isNotEmpty()) { "At least one location is required" }
val r = requireNotNull(radius) { "radius is required" }
return SnapRequest(locations = locations.toList(), radius = r, id = id)
}
}
/** Java-friendly builder counterpart. */
class SnapRequestBuilderJ {
private val locations = mutableListOf<List<Double>>()
private var radius: Int? = null
private var id: String? = null
fun location(lon: Double, lat: Double) = apply { locations.add(listOf(lon, lat)) }
fun radius(meters: Int) = apply { this.radius = meters }
fun id(id: String?) = apply { this.id = id }
fun build(): SnapRequest {
require(locations.isNotEmpty()) { "At least one location is required" }
val r = requireNotNull(radius) { "radius is required" }
return SnapRequest(locations = locations.toList(), radius = r, id = id)
}
}

View File

@@ -1,8 +1,8 @@
package org.nitri.ors.model.snap
package org.nitri.ors.domain.snap
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.nitri.ors.model.meta.Metadata
import org.nitri.ors.domain.meta.Metadata
/**
* Snap response in GeoJSON format

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.model.snap
package org.nitri.ors.domain.snap
import kotlinx.serialization.Serializable

View File

@@ -1,8 +1,8 @@
package org.nitri.ors.model.snap
package org.nitri.ors.domain.snap
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.nitri.ors.model.meta.Metadata
import org.nitri.ors.domain.meta.Metadata
/**
* Response for ORS /v2/snap/{profile}[/json]

View File

@@ -1,29 +1,23 @@
package org.nitri.ors.repository
package org.nitri.ors.helper
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.elevation.ElevationLineRequest
import org.nitri.ors.model.elevation.ElevationLineResponse
import org.nitri.ors.model.elevation.ElevationPointRequest
import org.nitri.ors.model.elevation.ElevationPointResponse
import org.nitri.ors.OrsClient
import org.nitri.ors.domain.elevation.ElevationLineRequest
import org.nitri.ors.domain.elevation.ElevationLineResponse
import org.nitri.ors.domain.elevation.ElevationPointRequest
import org.nitri.ors.domain.elevation.ElevationPointResponse
class ElevationRepository(private val api: OpenRouteServiceApi) {
/**
* Calls the ORS elevation/line POST endpoint with a prepared request.
*/
suspend fun getElevationLine(request: ElevationLineRequest): ElevationLineResponse =
api.getElevationLine(request)
/** Helper functions for the ORS elevation endpoints. */
class ElevationHelper {
/**
* Convenience helper to request elevation for a LineString provided as list of [lon, lat] pairs.
* Builds a GeoJSON LineString payload and requests GeoJSON output.
*/
suspend fun getElevationLine(
suspend fun OrsClient.getElevationLine(
coordinates: List<List<Double>>, // [[lon,lat], [lon,lat], ...]
dataset: String? = null,
formatOut: String = "geojson",
@@ -44,7 +38,7 @@ class ElevationRepository(private val api: OpenRouteServiceApi) {
geometry = geometry,
dataset = dataset
)
return api.getElevationLine(request)
return getElevationLine(request)
}
/**
@@ -55,7 +49,7 @@ class ElevationRepository(private val api: OpenRouteServiceApi) {
* @param formatOut either "geojson" or "point"
* @param dataset optional dataset (e.g., "srtm")
*/
suspend fun getElevationPoint(
suspend fun OrsClient.getElevationPoint(
lon: Double,
lat: Double,
formatOut: String = "geojson",
@@ -67,6 +61,6 @@ class ElevationRepository(private val api: OpenRouteServiceApi) {
dataset = dataset,
geometry = listOf(lon, lat)
)
return api.getElevationPoint(request)
return getElevationPoint(request)
}
}

View File

@@ -0,0 +1,41 @@
package org.nitri.ors.helper
import org.nitri.ors.OrsClient
import org.nitri.ors.Profile
import org.nitri.ors.domain.export.ExportRequest
import org.nitri.ors.domain.export.ExportResponse
import org.nitri.ors.domain.export.TopoJsonExportResponse
/** Helpers for the export endpoints. */
class ExportHelper {
/** Requests the export endpoint with bounding box [bbox]. */
suspend fun OrsClient.export(bbox: List<List<Double>>, geometry: Boolean? = null, profile: Profile): ExportResponse {
val request = ExportRequest(
bbox = bbox,
id = "export_request",
geometry = geometry
)
return export(profile, request)
}
/** Requests the export endpoint asking explicitly for JSON output. */
suspend fun OrsClient.exportJson(bbox: List<List<Double>>, geometry: Boolean? = null, profile: Profile): ExportResponse {
val request = ExportRequest(
bbox = bbox,
id = "export_request_json",
geometry = geometry
)
return exportJson(profile, request)
}
/** Requests the export endpoint with TopoJSON output. */
suspend fun OrsClient.exportTopoJson(bbox: List<List<Double>>, geometry: Boolean? = null, profile: Profile): TopoJsonExportResponse {
val request = ExportRequest(
bbox = bbox,
id = "export_request_topo_json",
geometry = geometry
)
return exportTopoJson(profile, request)
}
}

View File

@@ -1,21 +1,19 @@
package org.nitri.ors.repository
package org.nitri.ors.helper
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.geocode.GeocodeSearchResponse
import org.nitri.ors.OrsClient
import org.nitri.ors.domain.geocode.GeocodeSearchResponse
/**
* Repository for ORS Geocoding endpoints using GET requests only.
*
* Note: ORS Pelias geocoding requires an api_key query parameter even though
* the client also sends an Authorization header. Therefore, methods here accept
* apiKey explicitly and pass it through to the API interface.
* Methods are member extensions on [OrsClient] and delegate to it.
*/
class GeocodeRepository(private val api: OpenRouteServiceApi) {
class GeocodeHelper {
/**
* Forward geocoding search.
*/
suspend fun search(
suspend fun OrsClient.search(
text: String,
apiKey: String,
focusLon: Double? = null,
@@ -33,8 +31,9 @@ class GeocodeRepository(private val api: OpenRouteServiceApi) {
layersCsv: String? = null,
size: Int? = 10,
): GeocodeSearchResponse {
return api.geocodeSearch(
return geocodeSearch(
text = text,
apiKey = apiKey,
focusLon = focusLon,
focusLat = focusLat,
rectMinLon = rectMinLon,
@@ -48,15 +47,14 @@ class GeocodeRepository(private val api: OpenRouteServiceApi) {
boundaryCountry = boundaryCountry,
sourcesCsv = sourcesCsv,
layersCsv = layersCsv,
size = size,
apiKey = apiKey
size = size
)
}
/**
* Autocomplete search; returns suggestions for a partial query.
*/
suspend fun autocomplete(
suspend fun OrsClient.autocomplete(
apiKey: String,
text: String,
focusLon: Double? = null,
@@ -73,7 +71,7 @@ class GeocodeRepository(private val api: OpenRouteServiceApi) {
layers: List<String>? = null,
size: Int? = null,
): GeocodeSearchResponse {
return api.autocomplete(
return geocodeAutocomplete(
apiKey = apiKey,
text = text,
focusLon = focusLon,
@@ -95,7 +93,7 @@ class GeocodeRepository(private val api: OpenRouteServiceApi) {
/**
* Structured forward geocoding using address fields.
*/
suspend fun structured(
suspend fun OrsClient.structured(
apiKey: String,
address: String? = null,
neighbourhood: String? = null,
@@ -119,7 +117,7 @@ class GeocodeRepository(private val api: OpenRouteServiceApi) {
sources: List<String>? = null,
size: Int? = null,
): GeocodeSearchResponse {
return api.geocodeStructured(
return geocodeStructured(
apiKey = apiKey,
address = address,
neighbourhood = neighbourhood,
@@ -148,7 +146,7 @@ class GeocodeRepository(private val api: OpenRouteServiceApi) {
/**
* Reverse geocoding for a point.
*/
suspend fun reverse(
suspend fun OrsClient.reverse(
apiKey: String,
lon: Double,
lat: Double,
@@ -158,7 +156,7 @@ class GeocodeRepository(private val api: OpenRouteServiceApi) {
sources: List<String>? = null,
boundaryCountry: String? = null,
): GeocodeSearchResponse {
return api.geocodeReverse(
return geocodeReverse(
apiKey = apiKey,
lon = lon,
lat = lat,

View File

@@ -0,0 +1,24 @@
package org.nitri.ors.helper
import org.nitri.ors.OrsClient
import org.nitri.ors.Profile
import org.nitri.ors.domain.isochrones.IsochronesRequest
import org.nitri.ors.domain.isochrones.IsochronesResponse
/** Helpers for requesting isochrones. */
class IsochronesHelper {
/** Calls the isochrones endpoint with the given parameters. */
suspend fun OrsClient.getIsochrones(
locations: List<List<Double>>,
range: List<Int>,
profile: Profile,
attributes: List<String>? = null,
rangeType: String? = null,
): IsochronesResponse {
val request = IsochronesRequest(
locations, range, rangeType, attributes
)
return getIsochrones(profile, request)
}
}

View File

@@ -0,0 +1,31 @@
package org.nitri.ors.helper
import org.nitri.ors.OrsClient
import org.nitri.ors.Profile
import org.nitri.ors.domain.matrix.MatrixRequest
import org.nitri.ors.domain.matrix.MatrixResponse
/** Helpers for the matrix endpoint. */
class MatrixHelper {
/** Calls the ORS Matrix endpoint for the given [profile]. */
suspend fun OrsClient.getMatrix(
locations: List<List<Double>>,
profile: Profile,
metrics: List<String>? = null,
sources: List<Int>? = null,
destinations: List<Int>? = null,
resolveLocations: Boolean? = null,
id: String? = null,
): MatrixResponse {
val request = MatrixRequest(
locations = locations,
destinations = destinations,
id = id,
metrics = metrics,
resolveLocations = resolveLocations,
sources = sources
)
return getMatrix(profile, request)
}
}

View File

@@ -0,0 +1,36 @@
package org.nitri.ors.helper
import kotlinx.serialization.json.JsonElement
import org.nitri.ors.OrsClient
import org.nitri.ors.domain.optimization.CustomMatrix
import org.nitri.ors.domain.optimization.Job
import org.nitri.ors.domain.optimization.OptimizationRequest
import org.nitri.ors.domain.optimization.OptimizationResponse
import org.nitri.ors.domain.optimization.Shipment
import org.nitri.ors.domain.optimization.Vehicle
/**
* Repository for the OpenRouteService Optimization endpoint using [OrsClient].
*/
class OptimizationHelper {
/**
* Calls the ORS Optimization endpoint with provided arguments and builds the request.
*/
suspend fun OrsClient.getOptimization(
vehicles: List<Vehicle>,
jobs: List<Job>? = null,
shipments: List<Shipment>? = null,
matrices: Map<String, CustomMatrix>? = null,
options: Map<String, JsonElement>? = null
): OptimizationResponse {
val request = OptimizationRequest(
jobs = jobs,
shipments = shipments,
vehicles = vehicles,
matrices = matrices,
options = options
)
return getOptimization(request)
}
}

View File

@@ -1,12 +1,13 @@
package org.nitri.ors.repository
package org.nitri.ors.helper
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.pois.GeoJsonGeometry
import org.nitri.ors.model.pois.Geometry
import org.nitri.ors.model.pois.PoisGeoJsonResponse
import org.nitri.ors.model.pois.PoisRequest
import org.nitri.ors.OrsClient
import org.nitri.ors.domain.pois.GeoJsonGeometry
import org.nitri.ors.domain.pois.Geometry
import org.nitri.ors.domain.pois.PoisGeoJsonResponse
import org.nitri.ors.domain.pois.PoisRequest
class PoisRepository(private val api: OpenRouteServiceApi) {
/** Convenience extensions for the POIs endpoints. */
class PoisHelper {
/**
* Query POIs within a bounding box.
@@ -17,7 +18,7 @@ class PoisRepository(private val api: OpenRouteServiceApi) {
* @param sortby Optional sort field
* @param buffer Optional buffer in meters applied to the geometry
*/
suspend fun getPoisByBbox(
suspend fun OrsClient.getPoisByBbox(
bbox: List<List<Double>>,
filters: Map<String, String>? = null,
limit: Int? = null,
@@ -30,7 +31,7 @@ class PoisRepository(private val api: OpenRouteServiceApi) {
limit = limit,
sortby = sortby
)
return api.getPois(request)
return getPois(request)
}
/**
@@ -39,7 +40,7 @@ class PoisRepository(private val api: OpenRouteServiceApi) {
* @param point [lon, lat]
* @param buffer Buffer radius in meters
*/
suspend fun getPoisByPoint(
suspend fun OrsClient.getPoisByPoint(
point: List<Double>,
buffer: Int,
filters: Map<String, String>? = null,
@@ -55,6 +56,6 @@ class PoisRepository(private val api: OpenRouteServiceApi) {
limit = limit,
sortby = sortby
)
return api.getPois(request)
return getPois(request)
}
}

View File

@@ -0,0 +1,81 @@
package org.nitri.ors.helper
import org.nitri.ors.OrsClient
import org.nitri.ors.Profile
import org.nitri.ors.domain.route.GeoJsonRouteResponse
import org.nitri.ors.domain.route.RouteRequest
import org.nitri.ors.domain.route.RouteResponse
/**
* Convenience extensions for invoking the directions endpoints.
*/
class RouteHelper {
/** Converts a profile key string to the corresponding [Profile] enum. */
private fun profileFromKey(key: String): Profile =
Profile.entries.firstOrNull { it.key == key }
?: throw IllegalArgumentException("Unknown profile key: $key")
/**
* Retrieves a route between two coordinates.
*/
suspend fun OrsClient.getRoute(
start: Pair<Double, Double>,
end: Pair<Double, Double>,
profile: String
): RouteResponse {
val request = RouteRequest(
coordinates = listOf(
listOf(start.first, start.second),
listOf(end.first, end.second)
)
)
return getRoute(profileFromKey(profile), request)
}
/**
* Retrieves a route as GPX between two coordinates.
*/
suspend fun OrsClient.getRouteGpx(
start: Pair<Double, Double>,
end: Pair<Double, Double>,
profile: String
): String {
val request = RouteRequest(
coordinates = listOf(
listOf(start.first, start.second),
listOf(end.first, end.second)
)
)
return getRouteGpx(profileFromKey(profile), request)
}
/**
* Retrieves a route as GPX for an arbitrary coordinate list.
*/
suspend fun OrsClient.getRouteGpx(
coordinates: List<List<Double>>,
language: String,
profile: String
): String {
val request = RouteRequest(coordinates = coordinates, language = language)
return getRouteGpx(profileFromKey(profile), request)
}
/**
* Retrieves a route as GeoJSON feature collection.
*/
suspend fun OrsClient.getRouteGeoJson(
start: Pair<Double, Double>,
end: Pair<Double, Double>,
profile: Profile
): GeoJsonRouteResponse {
val request = RouteRequest(
coordinates = listOf(
listOf(start.first, start.second),
listOf(end.first, end.second)
)
)
return getRouteGeoJson(profile, request)
}
}

View File

@@ -0,0 +1,56 @@
package org.nitri.ors.helper
import org.nitri.ors.OrsClient
import org.nitri.ors.Profile
import org.nitri.ors.domain.snap.SnapGeoJsonResponse
import org.nitri.ors.domain.snap.SnapRequest
import org.nitri.ors.domain.snap.SnapResponse
/** Helpers for the snap endpoints. */
class SnapHelper {
/** Calls the ORS Snap endpoint for the given [profile]. */
suspend fun OrsClient.getSnap(
locations: List<List<Double>>,
radius: Int,
profile: Profile,
id: String? = null,
): SnapResponse {
val request = SnapRequest(
locations = locations,
radius = radius,
id = id
)
return getSnap(profile, request)
}
/** Calls the ORS Snap JSON endpoint. */
suspend fun OrsClient.getSnapJson(
locations: List<List<Double>>,
radius: Int,
profile: Profile,
id: String? = null,
): SnapResponse {
val request = SnapRequest(
locations = locations,
radius = radius,
id = id
)
return getSnapJson(profile, request)
}
/** Calls the ORS Snap GeoJSON endpoint. */
suspend fun OrsClient.getSnapGeoJson(
locations: List<List<Double>>,
radius: Int,
profile: Profile,
id: String? = null,
): SnapGeoJsonResponse {
val request = SnapRequest(
locations = locations,
radius = radius,
id = id
)
return getSnapGeoJson(profile, request)
}
}

View File

@@ -1,17 +0,0 @@
package org.nitri.ors.model.elevation
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ElevationPointRequest(
@SerialName("format_in")
val formatIn: String, // Input format, must be provided (e.g., "point")
@SerialName("format_out")
val formatOut: String = "geojson", // "geojson" or "point"
val dataset: String? = null, // Optional dataset, e.g. "srtm"
val geometry: List<Double> // [lon, lat]
)

View File

@@ -1,10 +0,0 @@
package org.nitri.ors.model.export
import kotlinx.serialization.Serializable
@Serializable
data class ExportRequest(
val bbox: List<List<Double>>, // [minLon, minLat, maxLon, maxLat]
val id: String,
val geometry: Boolean? = null
)

View File

@@ -1,41 +0,0 @@
package org.nitri.ors.model.export
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TopoJsonExportResponse(
val type: String, // "Topology"
val objects: TopoObjects,
/** Top-level arcs are arrays of [lon, lat] Double coordinates */
val arcs: List<List<List<Double>>>,
val bbox: List<Double> // [minLon, minLat, maxLon, maxLat]
)
@Serializable
data class TopoObjects(
val network: GeometryCollection
)
@Serializable
data class GeometryCollection(
val type: String, // "GeometryCollection"
val geometries: List<TopoGeometry>
)
@Serializable
data class TopoGeometry(
val type: String, // "LineString" (currently)
/** These are indices into the top-level arcs array (can be negative to indicate reversal) */
val arcs: List<Int>,
val properties: GeometryProps? = null // be lenient; fields can vary
)
@Serializable
data class GeometryProps(
// weight often arrives as a string in this endpoint
val weight: String? = null,
@SerialName("node_from") val nodeFrom: Long? = null,
@SerialName("node_to") val nodeTo: Long? = null
)

View File

@@ -1,26 +0,0 @@
package org.nitri.ors.model.pois
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable
@Serializable
data class PoisRequest(
@Required val request: String = "pois",
val geometry: Geometry,
val filters: Map<String, String>? = null,
val limit: Int? = null,
val sortby: String? = null
)
@Serializable
data class Geometry(
val bbox: List<List<Double>>? = null, // [[minLon,minLat],[maxLon,maxLat]]
val geojson: GeoJsonGeometry? = null, // optional: GeoJSON geometry
val buffer: Int? = null // optional: buffer in meters
)
@Serializable
data class GeoJsonGeometry(
val type: String, // e.g., "Point"
val coordinates: List<Double> // [lon, lat]
)

View File

@@ -1,36 +0,0 @@
package org.nitri.ors.repository
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.export.ExportRequest
import org.nitri.ors.model.export.ExportResponse
import org.nitri.ors.model.export.TopoJsonExportResponse
class ExportRepository(private val api: OpenRouteServiceApi) {
suspend fun export(bbox: List<List<Double>>, geometry: Boolean? = null, profile: String): ExportResponse {
val request = ExportRequest(
bbox = bbox,
id = "export_request",
geometry = geometry
)
return api.export(profile, request)
}
suspend fun exportJson(bbox: List<List<Double>>, geometry: Boolean? = null, profile: String): ExportResponse {
val request = ExportRequest(
bbox = bbox,
id = "export_request_json",
geometry = geometry
)
return api.exportJson(profile, request)
}
suspend fun exportTopoJson(bbox: List<List<Double>>, geometry: Boolean? = null, profile: String): TopoJsonExportResponse {
val request = ExportRequest(
bbox = bbox,
id = "export_request_topo_json",
geometry = geometry
)
return api.exportTopoJson(profile, request)
}
}

View File

@@ -1,24 +0,0 @@
package org.nitri.ors.repository
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.export.ExportRequest
import org.nitri.ors.model.export.ExportResponse
import org.nitri.ors.model.export.TopoJsonExportResponse
import org.nitri.ors.model.isochrones.IsochronesRequest
import org.nitri.ors.model.isochrones.IsochronesResponse
class IsochronesRepository(private val api: OpenRouteServiceApi) {
suspend fun getIsochrones(
locations: List<List<Double>>,
range: List<Int>,
profile: String,
attributes: List<String>? = null,
rangeType: String? = null,
): IsochronesResponse {
val request = IsochronesRequest(
locations, range, rangeType, attributes
)
return api.getIsochrones(profile, request)
}
}

View File

@@ -1,39 +0,0 @@
package org.nitri.ors.repository
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.matrix.MatrixRequest
import org.nitri.ors.model.matrix.MatrixResponse
class MatrixRepository(private val api: OpenRouteServiceApi) {
/**
* Calls the ORS Matrix endpoint for the given profile.
*
* @param locations List of [lon, lat] coordinate pairs.
* @param profile ORS profile, e.g. "driving-car", "foot-hiking", etc.
* @param metrics Optional list of metrics to include (e.g., ["duration"], ["distance"], or both).
* @param sources Optional list of indices into locations used as sources.
* @param destinations Optional list of indices into locations used as destinations.
* @param resolveLocations Optional flag to resolve/snaps locations to the network.
* @param id Optional arbitrary request id.
*/
suspend fun getMatrix(
locations: List<List<Double>>,
profile: String,
metrics: List<String>? = null,
sources: List<Int>? = null,
destinations: List<Int>? = null,
resolveLocations: Boolean? = null,
id: String? = null,
): MatrixResponse {
val request = MatrixRequest(
locations = locations,
destinations = destinations,
id = id,
metrics = metrics,
resolveLocations = resolveLocations,
sources = sources
)
return api.getMatrix(profile, request)
}
}

View File

@@ -1,46 +0,0 @@
package org.nitri.ors.repository
import kotlinx.serialization.json.JsonElement
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.optimization.CustomMatrix
import org.nitri.ors.model.optimization.Job
import org.nitri.ors.model.optimization.OptimizationRequest
import org.nitri.ors.model.optimization.OptimizationResponse
import org.nitri.ors.model.optimization.Shipment
import org.nitri.ors.model.optimization.Vehicle
/**
* Repository for the OpenRouteService Optimization endpoint.
*
* This is a thin wrapper around the Retrofit API, similar to other repositories
* in this package. The repository builds the OptimizationRequest from the
* provided arguments.
*/
class OptimizationRepository(private val api: OpenRouteServiceApi) {
/**
* Calls the ORS Optimization endpoint with provided arguments and builds the request.
*
* @param vehicles Required list of vehicles.
* @param jobs Optional list of jobs.
* @param shipments Optional list of shipments.
* @param matrices Optional custom matrices keyed by profile.
* @param options Optional free-form options.
*/
suspend fun getOptimization(
vehicles: List<Vehicle>,
jobs: List<Job>? = null,
shipments: List<Shipment>? = null,
matrices: Map<String, CustomMatrix>? = null,
options: Map<String, JsonElement>? = null
): OptimizationResponse {
val request = OptimizationRequest(
jobs = jobs,
shipments = shipments,
vehicles = vehicles,
matrices = matrices,
options = options
)
return api.getOptimization(request)
}
}

View File

@@ -1,37 +0,0 @@
package org.nitri.ors.repository
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.route.GeoJsonRouteResponse
import org.nitri.ors.model.route.RouteRequest
import org.nitri.ors.model.route.RouteResponse
class RouteRepository(private val api: OpenRouteServiceApi) {
suspend fun getRoute(start: Pair<Double, Double>, end: Pair<Double, Double>, profile: String): RouteResponse {
val request = RouteRequest(coordinates = listOf(
listOf(start.first, start.second),
listOf(end.first, end.second)
))
return api.getRoute(profile, request)
}
suspend fun getRouteGpx(start: Pair<Double, Double>, end: Pair<Double, Double>, profile: String): String {
val request = RouteRequest(coordinates = listOf(
listOf(start.first, start.second),
listOf(end.first, end.second)
))
return api.getRouteGpx(profile, request).body()?.string() ?: ""
}
suspend fun getRouteGpx(coordinates: List<List<Double>>, language: String, profile: String): String {
val request = RouteRequest(coordinates = coordinates, language = language)
return api.getRouteGpx(profile, request).body()?.string() ?: ""
}
suspend fun getRouteGeoJson(start: Pair<Double, Double>, end: Pair<Double, Double>, profile: String): GeoJsonRouteResponse {
val request = RouteRequest(coordinates = listOf(
listOf(start.first, start.second),
listOf(end.first, end.second)
))
return api.getRouteGeoJson(profile, request)
}
}

View File

@@ -1,65 +0,0 @@
package org.nitri.ors.repository
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.snap.SnapGeoJsonResponse
import org.nitri.ors.model.snap.SnapRequest
import org.nitri.ors.model.snap.SnapResponse
class SnapRepository(private val api: OpenRouteServiceApi) {
/**
* Calls the ORS Snap endpoint for the given profile.
*
* @param locations List of [lon, lat] coordinates to snap.
* @param radius Maximum radius (meters) around given coordinates to search for graph edges.
* @param profile ORS profile, e.g. "driving-car", "foot-hiking", etc.
* @param id Optional arbitrary request id echoed back by the service.
*/
suspend fun getSnap(
locations: List<List<Double>>,
radius: Int,
profile: String,
id: String? = null,
): SnapResponse {
val request = SnapRequest(
locations = locations,
radius = radius,
id = id
)
return api.getSnap(profile, request)
}
/**
* Calls the ORS Snap JSON endpoint.
*/
suspend fun getSnapJson(
locations: List<List<Double>>,
radius: Int,
profile: String,
id: String? = null,
): SnapResponse {
val request = SnapRequest(
locations = locations,
radius = radius,
id = id
)
return api.getSnapJson(profile, request)
}
/**
* Calls the ORS Snap GeoJSON endpoint.
*/
suspend fun getSnapGeoJson(
locations: List<List<Double>>,
radius: Int,
profile: String,
id: String? = null,
): SnapGeoJsonResponse {
val request = SnapRequest(
locations = locations,
radius = radius,
id = id
)
return api.getSnapGeoJson(profile, request)
}
}

View File

@@ -1,4 +1,4 @@
package org.nitri.ors.client
package org.nitri.ors.restclient
import android.content.Context
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
@@ -11,14 +11,28 @@ import org.nitri.ors.api.OpenRouteServiceApi
import okhttp3.MediaType.Companion.toMediaType
import java.util.concurrent.TimeUnit
object OpenRouteServiceClient {
/**
* Builds a Retrofit HTTP client configured for the public
* [OpenRouteService](https://openrouteservice.org/) API.
*/
object OpenRouteServiceRestClient {
/**
* Creates an [OpenRouteServiceApi] with authorization and sensible defaults.
*
* @param apiKey ORS API key used in the Authorization header
* @param context Android context used for user-agent construction
*/
fun create(apiKey: String, context: Context): OpenRouteServiceApi {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val appVersion = packageInfo.versionName ?: "unknown"
val userAgent = "${packageInfo.packageName}/$appVersion (ORS-Android-Client)"
val json = Json { ignoreUnknownKeys = true }
val json = Json {
ignoreUnknownKeys = true
isLenient = true
allowSpecialFloatingPointValues = true
}
val client = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
@@ -36,6 +50,32 @@ object OpenRouteServiceClient {
chain.proceed(newRequest)
}
// Retry on 429 Too Many Requests with simple backoff honoring Retry-After when present
.addInterceptor { chain ->
val maxAttempts = 3
var waitMs = 500L
var req = chain.request()
repeat(maxAttempts) { attempt ->
val response = chain.proceed(req)
if (response.code != 429 || attempt == maxAttempts - 1) {
return@addInterceptor response
}
val retryAfter = response.header("Retry-After")
response.close()
val delayMs = retryAfter?.toLongOrNull()?.times(1000) ?: waitMs
try {
Thread.sleep(delayMs)
} catch (_: InterruptedException) {
return@addInterceptor chain.proceed(req)
}
waitMs *= 2
}
// compiler sees this as unreachable
error("Unexpected state: retry loop exited without returning")
}
// Increase timeouts to accommodate heavier endpoints like POIs
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)

View File

@@ -0,0 +1,49 @@
package org.nitri.ors
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Test
import org.nitri.ors.domain.export.ExportResponse
class ExportResponseStringAsDoubleSerializerTest {
private val json = Json { ignoreUnknownKeys = true }
@Test
fun parses_weight_when_number_or_string() {
val payload = """
{
"nodes": [],
"edges": [
{"fromId":1, "toId":2, "weight": "12.34"},
{"fromId":2, "toId":3, "weight": 56.78}
]
}
""".trimIndent()
val resp = json.decodeFromString<ExportResponse>(payload)
assertEquals(2, resp.edges.size)
assertEquals(12.34, resp.edges[0].weight, 1e-9)
assertEquals(56.78, resp.edges[1].weight, 1e-9)
}
@Test
fun serializes_weight_as_number() {
val payload = """
{
"nodes": [],
"edges": [
{"fromId":1, "toId":2, "weight": "10"}
]
}
""".trimIndent()
val resp = json.decodeFromString<ExportResponse>(payload)
val out = json.encodeToString(resp)
// The serializer writes numbers, so we expect \"weight\":10.0 (or 10) in output
// We assert that there is no quoted weight in the serialized JSON.
assert(!out.contains("\"weight\": \""))
}
}

View File

@@ -1,28 +1,26 @@
package org.nitri.ors
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.nitri.ors.api.OpenRouteServiceApi
import org.nitri.ors.model.route.Route
import org.nitri.ors.model.route.RouteRequest
import org.nitri.ors.model.route.RouteResponse
import org.nitri.ors.model.route.RouteSummary
import org.nitri.ors.repository.RouteRepository
import org.nitri.ors.domain.route.Route
import org.nitri.ors.domain.route.RouteRequest
import org.nitri.ors.domain.route.RouteResponse
import org.nitri.ors.domain.route.RouteSummary
import org.nitri.ors.helper.RouteHelper
class RouteRepositoryTest {
class RouteHelperTest {
private lateinit var api: OpenRouteServiceApi
private lateinit var repository: RouteRepository
private lateinit var client: OrsClient
private lateinit var routeHelper: RouteHelper
@Before
fun setUp() {
api = mock(OpenRouteServiceApi::class.java)
repository = RouteRepository(api)
client = mock(OrsClient::class.java)
routeHelper = RouteHelper()
}
@Test
@@ -49,11 +47,11 @@ class RouteRepositoryTest {
)
)
`when`(api.getRoute(profile, expectedRequest)).thenReturn(expectedResponse)
`when`(client.getRoute(Profile.DRIVING_CAR, expectedRequest)).thenReturn(expectedResponse)
val result = repository.getRoute(start, end, profile)
val result = with(routeHelper) { client.getRoute(start, end, profile) }
assert(result == expectedResponse)
verify(api).getRoute(profile, expectedRequest)
verify(client).getRoute(Profile.DRIVING_CAR, expectedRequest)
}
}

View File

@@ -0,0 +1,33 @@
package org.nitri.ors
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.nitri.ors.domain.route.RouteRequestBuilderJ
class RouteRequestBuilderJTest {
@Test
fun builds_coordinates_and_language() {
val req = RouteRequestBuilderJ()
.start(8.68, 49.41)
.add(8.69, 49.42)
.end(8.70, 49.43)
.language("de")
.build()
assertEquals(3, req.coordinates.size)
assertEquals(listOf(8.68, 49.41), req.coordinates.first())
assertEquals(listOf(8.70, 49.43), req.coordinates.last())
assertEquals("de", req.language)
}
@Test
fun throws_when_less_than_two_points() {
assertThrows(IllegalArgumentException::class.java) {
RouteRequestBuilderJ()
.start(8.68, 49.41)
.build()
}
}
}

View File

@@ -0,0 +1,35 @@
package org.nitri.ors
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.nitri.ors.domain.snap.SnapRequestBuilderJ
class SnapRequestBuilderTest {
@Test
fun builds_snap_request() {
val req = SnapRequestBuilderJ()
.location(8.0, 48.0)
.radius(25)
.id("jid")
.build()
assertEquals(1, req.locations.size)
assertEquals(25, req.radius)
assertEquals("jid", req.id)
}
@Test
fun requires_location_and_radius() {
assertThrows(IllegalArgumentException::class.java) {
SnapRequestBuilderJ()
.radius(10)
.build()
}
assertThrows(IllegalArgumentException::class.java) {
SnapRequestBuilderJ()
.location(8.0, 48.0)
.build()
}
}
}