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

Remove ORS module

This commit is contained in:
Pygmalion69
2025-09-07 13:25:16 +02:00
parent 4a72876bd5
commit d243114f63
79 changed files with 7 additions and 4711 deletions

View File

@@ -99,15 +99,14 @@ android {
}
dependencies {
implementation project(':ors-client')
implementation fileTree(dir: 'libs', include: ['*.jar'])
//noinspection GradleCompatible
implementation 'androidx.core:core-ktx:1.16.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2"
implementation 'androidx.core:core-ktx:1.17.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.3"
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.android.material:material:1.13.0'
implementation 'org.osmdroid:osmdroid-android:6.1.20'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.github.PhilJay:MPAndroidChart:v3.0.3'
@@ -135,8 +134,9 @@ dependencies {
}
playImplementation 'com.google.android.gms:play-services-ads:24.5.0'
playImplementation 'com.google.android.ump:user-messaging-platform:3.2.0'
implementation 'com.github.Pygmalion69:ors-android-client:0.1.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
}

View File

@@ -47,7 +47,6 @@ import org.nitri.opentopo.nearby.entity.NearbyItem
import org.nitri.opentopo.util.Utils
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

View File

@@ -26,6 +26,7 @@ allprojects {
password = System.getenv("GPR_TOKEN") ?: findProperty("gpr.token")
}
}
maven { url = uri("https://jitpack.io") }
}
tasks.withType(JavaCompile).tap {
configureEach {

View File

@@ -1,2 +0,0 @@
/build
/src/main/res/values/api_key.xml

View File

@@ -1,73 +0,0 @@
import java.io.ByteArrayOutputStream
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0"
id("kotlin-kapt")
}
android {
namespace = "org.nitri.ors"
compileSdk = 36
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
testOptions {
targetSdk = 35
}
lint {
targetSdk = 35
}
publishing { singleVariant("release") { withSourcesJar() } }
}
dependencies {
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("com.google.android.material:material:1.12.0")
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("androidx.test:monitor:1.8.0")
implementation("androidx.test.ext:junit-ktx:1.3.0")
// Unit testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
testImplementation("org.mockito:mockito-core:5.19.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:6.0.0")
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test:rules:1.7.0")
}

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,10 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.nitri.ors.test">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="ORS Test"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:allowBackup="true" />
</manifest>

View File

@@ -1,65 +0,0 @@
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

@@ -1,74 +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.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.helper.ElevationHelper
@RunWith(AndroidJUnit4::class)
class ElevationInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, ElevationHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val helper = ElevationHelper()
return client to helper
}
@Test
fun testElevation_point_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, helper) = create(context)
// A point near Heidelberg, Germany
val lon = 8.681495
val lat = 49.41461
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)
assertEquals("Geometry type should be Point", "Point", response.geometry.type)
val coords = response.geometry.coordinates
assertTrue("Point coordinates should contain at least [lon, lat]", coords.size >= 2)
// Typically API returns elevation as third value
if (coords.size >= 3) {
// elevation could be any double, just ensure it's a number
val elevation = coords[2]
assertTrue("Elevation should be a finite number", elevation.isFinite())
}
}
@Test
fun testElevation_line_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, helper) = create(context)
// A short line segment around Heidelberg
val coordinates = listOf(
listOf(8.681495, 49.41461),
listOf(8.687872, 49.420318)
)
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)
val lineCoords = response.geometry.coordinates
assertTrue("LineString should have at least 2 points", lineCoords.size >= 2)
val first = lineCoords.first()
assertTrue("Each coordinate should have at least [lon, lat]", first.size >= 2)
if (first.size >= 3) {
val elevation = first[2]
assertTrue("Elevation should be a finite number", elevation.isFinite())
}
}
}

View File

@@ -1,42 +0,0 @@
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

@@ -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.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,95 +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.helper.GeocodeHelper
@RunWith(AndroidJUnit4::class)
class GeocodeInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, GeocodeHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val helper = GeocodeHelper()
return client to helper
}
@Test
fun testGeocode_search_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, helper) = create(context)
val apiKey = context.getString(R.string.ors_api_key)
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())
val first = response.features.first()
// Basic geometry sanity
assertNotNull("First feature geometry should not be null", first.geometry)
val geom = first.geometry!!
assertTrue("Geometry coordinates should have at least [lon, lat]", geom.coordinates.size >= 2)
// Basic properties sanity
assertNotNull("First feature properties should not be null", first.properties)
val name = first.properties?.name ?: first.properties?.label
assertTrue("Feature should have a name or label", !name.isNullOrBlank())
}
@Test
fun testGeocode_reverse_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<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 = 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())
val first = response.features.first()
assertNotNull("First reverse feature properties should not be null", first.properties)
val label = first.properties?.label ?: first.properties?.name
assertTrue("Reverse feature should provide a label/name", !label.isNullOrBlank())
}
@Test
fun testGeocode_autocomplete_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, helper) = create(context)
val apiKey = context.getString(R.string.ors_api_key)
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

@@ -1,49 +0,0 @@
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

@@ -1,57 +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.helper.IsochronesHelper
@RunWith(AndroidJUnit4::class)
class IsochronesInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, IsochronesHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val helper = IsochronesHelper()
return client to helper
}
@Test
fun testIsochrones_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, helper) = create(context)
// Heidelberg, Germany [lon, lat]
val locations = listOf(
listOf(8.681495, 49.41461)
)
// 5 minutes (300 seconds)
val range = listOf(300)
val profile = Profile.DRIVING_CAR
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())
val first = response.features.first()
assertNotNull("Feature geometry should not be null", first.geometry)
// Basic metadata sanity
assertTrue("Response type should not be blank", response.type.isNotBlank())
assertNotNull("Metadata should be present", response.metadata)
assertTrue("BBox should have 4 numbers", response.bbox.size == 4)
}
}

View File

@@ -1,46 +0,0 @@
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

@@ -1,69 +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.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.helper.MatrixHelper
@RunWith(AndroidJUnit4::class)
class MatrixInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, MatrixHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val helper = MatrixHelper()
return client to helper
}
@Test
fun testMatrix_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<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 = Profile.DRIVING_CAR
val metrics = listOf("duration", "distance")
val response = with(helper) {
client.getMatrix(
locations = locations,
profile = profile,
metrics = metrics,
resolveLocations = false
)
}
assertNotNull("Matrix response should not be null", response)
// Durations and distances should be present when requested
val durations = response.durations
val distances = response.distances
assertNotNull("Durations should be present when requested", durations)
assertNotNull("Distances should be present when requested", distances)
// Expect a 2x2 matrix for two input points
durations!!
distances!!
assertEquals("Durations should have size equal to number of sources", 2, durations.size)
assertEquals("Distances should have size equal to number of sources", 2, distances.size)
assertEquals("Each durations row should have size equal to number of destinations", 2, durations[0].size)
assertEquals("Each distances row should have size equal to number of destinations", 2, distances[0].size)
// Sanity: diagonal (origin to same point) should be zero or near-zero for distance
assertTrue("Distance from a point to itself should be >= 0", distances[0][0] >= 0.0)
assertTrue("Duration from a point to itself should be >= 0", durations[0][0] >= 0.0)
// Metadata should be included
assertNotNull("Metadata should be present", response.metadata)
}
}

View File

@@ -1,43 +0,0 @@
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

@@ -1,72 +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.domain.optimization.Job
import org.nitri.ors.domain.optimization.Vehicle
import org.nitri.ors.helper.OptimizationHelper
@RunWith(AndroidJUnit4::class)
class OptimizationInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, OptimizationHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val helper = OptimizationHelper()
return client to helper
}
@Test
fun testOptimization_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, helper) = create(context)
// Simple scenario in/near Heidelberg, Germany
val vehicle = Vehicle(
id = 1,
profile = "driving-car",
// Start and end at/near Heidelberg castle parking
start = listOf(8.6910, 49.4100),
end = listOf(8.6910, 49.4100)
)
val jobs = listOf(
Job(
id = 101,
location = listOf(8.681495, 49.41461) // Heidelberg center
),
Job(
id = 102,
location = listOf(8.687872, 49.420318) // Nearby point
)
)
val response = with(helper) {
client.getOptimization(
vehicles = listOf(vehicle),
jobs = jobs
)
}
// Basic assertions
assertNotNull("Optimization response should not be null", response)
assertNotNull("Summary should be present", response.summary)
assertTrue("Routes list should be present", response.routes != null)
assertTrue("Routes should not be empty for solvable small case", response.routes.isNotEmpty())
val firstRoute = response.routes.first()
assertTrue("Route steps should not be empty", firstRoute.steps.isNotEmpty())
// Code is typically 0 for success in VROOM-like APIs; ensure non-negative as a safe check
assertTrue("Response code should be non-negative", response.code >= 0)
// Optional additional sanity checks
assertTrue("Total duration should be >= 0", response.summary.duration >= 0)
assertTrue("Total service should be >= 0", response.summary.service >= 0)
}
}

View File

@@ -1,34 +0,0 @@
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

@@ -1,58 +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.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.helper.PoisHelper
//@RunWith(AndroidJUnit4::class)
class PoisInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, PoisHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val helper = PoisHelper()
return client to helper
}
// 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

@@ -1,58 +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.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

@@ -1,63 +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.*
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.domain.route.GeoJsonRouteResponse
import org.nitri.ors.helper.RouteHelper
@RunWith(AndroidJUnit4::class)
class RouteInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, RouteHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val repo = RouteHelper()
return client to repo
}
@Test
fun testFetchRoute_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, repository) = create(context)
val start = Pair(8.681495, 49.41461)
val end = Pair(8.687872, 49.420318)
val route = with(repository) { client.getRoute(start, end, "driving-car") }
assertNotNull("Route should not be null", route)
}
@Test
fun testFetchGpxRoute_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, repository) = create(context)
val start = Pair(8.681495, 49.41461)
val end = Pair(8.687872, 49.420318)
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" }
}
@Test
fun testFetchGeoJsonRoute_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, repository) = create(context)
val start = Pair(8.681495, 49.41461)
val end = Pair(8.687872, 49.420318)
val route: GeoJsonRouteResponse = with(repository) { client.getRouteGeoJson(start, end,
Profile.DRIVING_CAR) }
assertNotNull("Route should not be null", route)
}
}

View File

@@ -1,57 +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.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

@@ -1,47 +0,0 @@
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

@@ -1,120 +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.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.nitri.ors.helper.SnapHelper
@RunWith(AndroidJUnit4::class)
class SnapInstrumentedTest {
private fun create(context: Context): Pair<DefaultOrsClient, SnapHelper> {
val apiKey = context.getString(R.string.ors_api_key)
val client = DefaultOrsClient(apiKey, context)
val helper = SnapHelper()
return client to helper
}
@Test
fun testSnap_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<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 = Profile.DRIVING_CAR
val radius = 50 // meters
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)
assertTrue("Locations should not be empty", response.locations.isNotEmpty())
assertEquals("Should have as many results as inputs", locations.size, response.locations.size)
val first = response.locations.first()
assertNotNull("First snapped location should have coordinates", first.location)
assertEquals("Snapped coordinates should have 2 values [lon, lat]", 2, first.location.size)
assertTrue("Snapped distance should be non-negative", first.snappedDistance >= 0.0)
}
@Test
fun testSnapJson_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, helper) = create(context)
val locations = listOf(
listOf(8.681495, 49.41461),
listOf(8.687872, 49.420318)
)
val profile = Profile.DRIVING_CAR
val radius = 50
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)
assertTrue("Locations should not be empty", response.locations.isNotEmpty())
response.locations.forEach { loc ->
assertEquals("Coordinate should be [lon, lat]", 2, loc.location.size)
assertTrue("Snapped distance should be non-negative", loc.snappedDistance >= 0.0)
}
}
@Test
fun testSnapGeoJson_successful() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val (client, repository) = create(context)
val locations = listOf(
listOf(8.681495, 49.41461),
listOf(8.687872, 49.420318)
)
val profile = Profile.DRIVING_CAR
val radius = 50
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)
assertNotNull("Metadata should be present", response.metadata)
assertTrue("Features should not be empty", response.features.isNotEmpty())
val feature = response.features.first()
assertEquals("Feature type should be Feature", "Feature", feature.type)
assertEquals("Geometry type should be Point", "Point", feature.geometry.type)
assertEquals("Point coordinates should be [lon, lat]", 2, feature.geometry.coordinates.size)
assertTrue("snapped_distance should be non-negative", feature.properties.snappedDistance >= 0.0)
assertTrue("source_id should be non-negative", feature.properties.sourceId >= 0)
}
}

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -1,300 +0,0 @@
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,17 +0,0 @@
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

@@ -1,196 +0,0 @@
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,261 +0,0 @@
package org.nitri.ors.api
import okhttp3.ResponseBody
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
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
interface OpenRouteServiceApi {
// Directions
@GET("v2/directions/{profile}")
suspend fun getRouteSimple(
@Path("profile") profile: String,
@Query("start") start: String, // e.g. "8.681495,49.41461"
@Query("end") end: String // e.g. "8.687872,49.420318"
): RouteResponse
@POST("v2/directions/{profile}")
suspend fun getRoute(
@Path("profile") profile: String,
@Body request: RouteRequest
): RouteResponse
@POST("v2/directions/{profile}/json")
suspend fun getRouteJson(
@Path("profile") profile: String,
@Body request: RouteRequest
): RouteResponse
@POST("v2/directions/{profile}/gpx")
suspend fun getRouteGpx(
@Path("profile") profile: String,
@Body request: RouteRequest
): Response<ResponseBody>
@POST("v2/directions/{profile}/geojson")
suspend fun getRouteGeoJson(
@Path("profile") profile: String,
@Body request: RouteRequest
): GeoJsonRouteResponse
// Export endpoints
@POST("v2/export/{profile}")
suspend fun export(
@Path("profile") profile: String,
@Body request: ExportRequest
): ExportResponse
@POST("v2/export/{profile}/json")
suspend fun exportJson(
@Path("profile") profile: String,
@Body request: ExportRequest
): ExportResponse
@POST("v2/export/{profile}/topojson")
suspend fun exportTopoJson(
@Path("profile") profile: String,
@Body request: ExportRequest
): TopoJsonExportResponse
// Isochrones endpoint
@POST("v2/isochrones/{profile}")
suspend fun getIsochrones(
@Path("profile") profile: String,
@Body request: IsochronesRequest
): IsochronesResponse
// Matrix endpoint
@POST("v2/matrix/{profile}")
suspend fun getMatrix(
@Path("profile") profile: String,
@Body request: MatrixRequest
): MatrixResponse
// Snapping
@POST("v2/snap/{profile}")
suspend fun getSnap(
@Path("profile") profile: String,
@Body request: SnapRequest
): SnapResponse
@POST("v2/snap/{profile}/json")
suspend fun getSnapJson(
@Path("profile") profile: String,
@Body request: SnapRequest
): SnapResponse
@POST("v2/snap/{profile}/geojson")
suspend fun getSnapGeoJson(
@Path("profile") profile: String,
@Body request: SnapRequest
): SnapGeoJsonResponse
// POIs
@POST("pois")
suspend fun getPois(
@Body request: PoisRequest
): PoisGeoJsonResponse
// Optimization
@POST("optimization")
suspend fun getOptimization(
@Body request: OptimizationRequest
): OptimizationResponse
// Elevation
@POST("elevation/line")
suspend fun getElevationLine(
@Body request: ElevationLineRequest
): ElevationLineResponse
@GET("elevation/point")
suspend fun getElevationPointSimple(
@Query("geometry") start: String, // e.g. "8.681495,49.41461"
): RouteResponse
@POST("elevation/point")
suspend fun getElevationPoint(
@Body request: ElevationPointRequest
): ElevationPointResponse
// Geocode
@GET("geocode/search")
suspend fun geocodeSearch(
@Query("text") text: String,
// Optional focus point
@Query("focus.point.lon") focusLon: Double? = null,
@Query("focus.point.lat") focusLat: Double? = null,
// Optional rectangular boundary
@Query("boundary.rect.min_lon") rectMinLon: Double? = null,
@Query("boundary.rect.min_lat") rectMinLat: Double? = null,
@Query("boundary.rect.max_lon") rectMaxLon: Double? = null,
@Query("boundary.rect.max_lat") rectMaxLat: Double? = null,
// Optional circular boundary
@Query("boundary.circle.lon") circleLon: Double? = null,
@Query("boundary.circle.lat") circleLat: Double? = null,
@Query("boundary.circle.radius") circleRadiusMeters: Double? = null,
// Other optional filters
@Query("boundary.gid") boundaryGid: String? = null,
// Pass comma-separated if multiple, e.g. "DE,AT"
@Query("boundary.country") boundaryCountry: String? = null,
// Pelias expects comma-separated values; join your list before passing.
@Query("sources") sourcesCsv: String? = null, // e.g. "osm,oa,gn,wof"
@Query("layers") layersCsv: String? = null, // e.g. "region,country,locality,address"
@Query("size") size: Int? = 10,
// Geocoder uses api_key as query
@Query("api_key") apiKey: String
): GeocodeSearchResponse
@GET("geocode/autocomplete")
suspend fun autocomplete(
@Query("api_key") apiKey: String,
@Query("text") text: String,
@Query("focus.point.lon") focusLon: Double? = null,
@Query("focus.point.lat") focusLat: Double? = null,
@Query("boundary.rect.min_lon") rectMinLon: Double? = null,
@Query("boundary.rect.min_lat") rectMinLat: Double? = null,
@Query("boundary.rect.max_lon") rectMaxLon: Double? = null,
@Query("boundary.rect.max_lat") rectMaxLat: Double? = null,
@Query("boundary.circle.lon") circleLon: Double? = null,
@Query("boundary.circle.lat") circleLat: Double? = null,
@Query("boundary.circle.radius") circleRadius: Double? = null,
@Query("boundary.country") country: String? = null,
@Query("sources") sources: List<String>? = null,
@Query("layers") layers: List<String>? = null,
@Query("size") size: Int? = null
): GeocodeSearchResponse
@GET("geocode/search/structured")
suspend fun geocodeStructured(
@Query("api_key") apiKey: String,
// Structured query parts (all optional; send only what you have)
@Query("address") address: String? = null,
@Query("neighbourhood") neighbourhood: String? = null,
@Query("borough") borough: String? = null,
@Query("locality") locality: String? = null, // e.g., city
@Query("county") county: String? = null,
@Query("region") region: String? = null, // e.g., state/province
@Query("country") country: String? = null, // ISO code or name
@Query("postalcode") postalcode: String? = null,
// Focus point (used for ranking)
@Query("focus.point.lon") focusLon: Double? = null,
@Query("focus.point.lat") focusLat: Double? = null,
// Bounding rectangle (optional)
@Query("boundary.rect.min_lon") rectMinLon: Double? = null,
@Query("boundary.rect.min_lat") rectMinLat: Double? = null,
@Query("boundary.rect.max_lon") rectMaxLon: Double? = null,
@Query("boundary.rect.max_lat") rectMaxLat: Double? = null,
// Bounding circle (optional)
@Query("boundary.circle.lon") circleLon: Double? = null,
@Query("boundary.circle.lat") circleLat: Double? = null,
@Query("boundary.circle.radius") circleRadiusMeters: Double? = null,
// Limit results to a specific country (ISO code)
@Query("boundary.country") boundaryCountry: String? = null,
// Filters
@Query("layers") layers: List<String>? = null, // e.g. ["address","venue","street","locality","region","country"]
@Query("sources") sources: List<String>? = null, // e.g. ["osm","oa","gn","wof"]
// Number of results (default 10)
@Query("size") size: Int? = null
): GeocodeSearchResponse
@GET("geocode/reverse")
suspend fun geocodeReverse(
@Query("api_key") apiKey: String,
// required
@Query("point.lon") lon: Double,
@Query("point.lat") lat: Double,
// optional ranking/filters
@Query("boundary.circle.radius") radiusKm: Double? = null, // Pelias expects km
@Query("size") size: Int? = null, // default 10
@Query("layers") layers: List<String>? = null, // e.g. ["address","venue"]
@Query("sources") sources: List<String>? = null, // e.g. ["osm","oa","gn","wof"]
@Query("boundary.country") boundaryCountry: String? = null // ISO code, e.g. "FR"
): GeocodeSearchResponse
}

View File

@@ -1,97 +0,0 @@
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,35 +0,0 @@
package org.nitri.ors.domain.elevation
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@Serializable
data class ElevationLineRequest(
/** One of: "geojson", "polyline", "encodedpolyline5", "encodedpolyline6" */
@SerialName("format_in") val formatIn: String,
/** One of: "geojson", "polyline", "encodedpolyline5", "encodedpolyline6" */
@SerialName("format_out") val formatOut: String,
/**
* Geometry payload. The API accepts different shapes depending on format:
* - format_in = "geojson" -> a GeoJSON LineString object
* - format_in = "polyline" -> [[lon,lat], [lon,lat], ...]
* - format_in = "encodedpolyline5/6"-> a single encoded polyline string
*
* Using JsonElement keeps this field flexible for all variants.
*/
val geometry: JsonElement,
/** Optional: pick a specific elevation dataset (e.g., "SRTM", "COP90", …) */
val dataset: String? = null
)
object ElevationFormats {
const val GEOJSON = "geojson"
const val POLYLINE = "polyline"
const val ENCODED_5 = "encodedpolyline5"
const val ENCODED_6 = "encodedpolyline6"
}

View File

@@ -1,18 +0,0 @@
package org.nitri.ors.domain.elevation
import kotlinx.serialization.Serializable
@Serializable
data class ElevationLineResponse(
val attribution: String? = null,
val geometry: ElevationLineGeometry,
val timestamp: Long? = null,
val version: String? = null
)
@Serializable
data class ElevationLineGeometry(
val type: String, // "LineString"
/** Coordinates with elevation: [lon, lat, ele] (API may return [lon,lat] if ele missing) */
val coordinates: List<List<Double>>
)

View File

@@ -1,22 +0,0 @@
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,17 +0,0 @@
package org.nitri.ors.domain.elevation
import kotlinx.serialization.Serializable
@Serializable
data class ElevationPointResponse(
val attribution: String,
val geometry: ElevationPointGeometry,
val timestamp: Long,
val version: String
)
@Serializable
data class ElevationPointGeometry(
val coordinates: List<Double>, // [lon, lat, elevation]
val type: String // "Point"
)

View File

@@ -1,45 +0,0 @@
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

@@ -1,14 +0,0 @@
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,59 +0,0 @@
package org.nitri.ors.domain.export
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import 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]` 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,
@Serializable(with = StringAsDoubleSerializer::class)
val weight: Double
)
/**
* ORS sometimes returns numeric fields (e.g., "weight") as strings.
* This serializer accepts either a JSON number or a string number.
*/
object StringAsDoubleSerializer : KSerializer<Double> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("StringAsDouble", PrimitiveKind.DOUBLE)
override fun deserialize(decoder: Decoder): Double {
return if (decoder is JsonDecoder) {
val prim = decoder.decodeJsonElement() as JsonPrimitive
prim.doubleOrNull ?: prim.content.toDouble()
} else {
decoder.decodeDouble()
}
}
override fun serialize(encoder: Encoder, value: Double) {
encoder.encodeDouble(value)
}
}

View File

@@ -1,42 +0,0 @@
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,157 +0,0 @@
package org.nitri.ors.domain.geocode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable
data class GeocodeSearchResponse(
val geocoding: Geocoding? = null,
val type: String? = null, // "FeatureCollection"
val features: List<GeocodeFeature> = emptyList(),
val bbox: List<Double>? = null,
val errors: List<String>? = null,
val warnings: List<String>? = null,
val engine: Engine? = null,
val timestamp: Long? = null
)
@Serializable
data class Geocoding(
val version: String? = null, // "0.2"
val attribution: String? = null,
val query: GeocodeQuery? = null
)
@Serializable
data class Engine(
val name: String? = null, // "Pelias"
val author: String? = null, // "Mapzen"
val version: String? = null
)
/**
* Pelias "query" block varies by endpoint; everything is optional.
* Fields like "point.lat" are addressed via @SerialName to match JSON.
*/
@Serializable
data class GeocodeQuery(
// common
val text: String? = null,
val size: Int? = null,
val layers: List<String>? = null,
val sources: List<String>? = null,
val private: Boolean? = null,
val parsed_text: JsonObject? = null, // present on some search/structured responses
val lang: PeliasLang? = null,
val querySize: Int? = null,
// focus point (search/autocomplete)
@SerialName("focus.point.lon") val focusPointLon: Double? = null,
@SerialName("focus.point.lat") val focusPointLat: Double? = null,
// reverse & circle bounds
@SerialName("point.lon") val pointLon: Double? = null,
@SerialName("point.lat") val pointLat: Double? = null,
@SerialName("boundary.circle.lon") val boundaryCircleLon: Double? = null,
@SerialName("boundary.circle.lat") val boundaryCircleLat: Double? = null,
@SerialName("boundary.circle.radius") val boundaryCircleRadius: Double? = null,
// rect bounds (search/autocomplete/structured)
@SerialName("boundary.rect.min_lon") val rectMinLon: Double? = null,
@SerialName("boundary.rect.min_lat") val rectMinLat: Double? = null,
@SerialName("boundary.rect.max_lon") val rectMaxLon: Double? = null,
@SerialName("boundary.rect.max_lat") val rectMaxLat: Double? = null,
// country limit (ISO-3166-1 alpha-2)
@SerialName("boundary.country") val boundaryCountry: String? = null,
// structured forward fields (all optional; API requires at least one)
val venue: String? = null,
val address: String? = null,
val neighbourhood: String? = null,
val borough: String? = null,
val locality: String? = null,
val county: String? = null,
val region: String? = null,
val country: String? = null,
val postcode: String? = null
)
@Serializable
data class PeliasLang(
val name: String? = null, // "English"
val iso6391: String? = null, // "en"
val iso6393: String? = null, // "eng"
val via: String? = null, // "header"
val defaulted: Boolean? = null
)
@Serializable
data class GeocodeFeature(
val type: String? = null, // "Feature"
val geometry: GeocodeGeometry? = null,
val properties: GeocodeProperties? = null,
val bbox: List<Double>? = null
)
@Serializable
data class GeocodeGeometry(
val type: String? = null, // typically "Point"
val coordinates: List<Double> = emptyList() // [lon, lat] (+ elevation if provided)
)
/**
* Properties are rich and vary by source; keep them optional.
* `addendum` is left as JsonObject to pass through extra provider-specific content (e.g., OSM).
*/
@Serializable
data class GeocodeProperties(
val id: String? = null,
val gid: String? = null,
val layer: String? = null,
val source: String? = null,
@SerialName("source_id") val sourceId: String? = null,
val name: String? = null,
val confidence: Double? = null,
val distance: Double? = null,
val accuracy: String? = null,
// address-ish
val label: String? = null,
val street: String? = null,
val housenumber: String? = null,
val postalcode: String? = null,
// admin hierarchy
val country: String? = null,
@SerialName("country_gid") val countryGid: String? = null,
@SerialName("country_a") val countryA: String? = null,
val macroregion: String? = null,
@SerialName("macroregion_gid") val macroregionGid: String? = null,
@SerialName("macroregion_a") val macroregionA: String? = null,
val region: String? = null,
@SerialName("region_gid") val regionGid: String? = null,
@SerialName("region_a") val regionA: String? = null,
val localadmin: String? = null,
@SerialName("localadmin_gid") val localadminGid: String? = null,
val locality: String? = null,
@SerialName("locality_gid") val localityGid: String? = null,
val borough: String? = null,
@SerialName("borough_gid") val boroughGid: String? = null,
val neighbourhood: String? = null,
@SerialName("neighbourhood_gid") val neighbourhoodGid: String? = null,
val continent: String? = null,
@SerialName("continent_gid") val continentGid: String? = null,
// provider extras: e.g. wheelchair, website, wikidata, etc.
val addendum: JsonObject? = null
)

View File

@@ -1,84 +0,0 @@
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,68 +0,0 @@
package org.nitri.ors.domain.isochrones
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
/**
* ORS Isochrones request
* Docs: /v2/isochrones/{profile}
*
* Required: locations, range
* Optional are nullable and omitted from JSON when null.
*/
@Serializable
data class IsochronesRequest(
/** [lon, lat] pairs */
val locations: List<List<Double>>,
/** In seconds for range_type="time" (default) or in meters for "distance". */
val range: List<Int>,
/** "time" (default) or "distance" */
@SerialName("range_type")
val rangeType: String? = null,
/** e.g. ["area"] (see docs for more) */
val attributes: List<String>? = null,
/** Optional request id that is echoed back in metadata */
val id: String? = null,
/** If true, include intersection info in response */
val intersections: Boolean? = null,
/** If set, returns multiple bands every `interval` (same unit as range_type) */
val interval: Int? = null,
/** "start" (default) or "destination" */
@SerialName("location_type")
val locationType: String? = null,
/** Smoothing factor, e.g. 25 */
val smoothing: Double? = null,
/** Extra tweaks */
val options: IsochronesOptions? = null
)
/**
* Subset of useful options. Extend as needed.
*/
@Serializable
data class IsochronesOptions(
/** "controlled" or "all" */
@SerialName("avoid_borders")
val avoidBorders: String? = null,
/** e.g. ["ferries","tollways"] */
@SerialName("avoid_features")
val avoidFeatures: List<String>? = null,
/**
* Avoid polygons (GeoJSON geometry). Keep generic to stay flexible:
* supply a Feature/Geometry object you serialize yourself.
*/
@SerialName("avoid_polygons")
val avoidPolygons: JsonElement? = null
)

View File

@@ -1,61 +0,0 @@
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,
val bbox: List<Double>,
val features: List<IsochroneFeature>,
val metadata: IsochronesMetadata
)
/** Individual isochrone feature. */
@Serializable
data class IsochroneFeature(
val type: String,
val properties: IsochroneProperties,
val geometry: IsochroneGeometry
)
@Serializable
data class IsochroneProperties(
@SerialName("group_index") val groupIndex: Int,
val value: Double, // <-- was Int
val center: List<Double>
)
/** Geometry of an isochrone feature. */
@Serializable
data class IsochroneGeometry(
val type: String,
val coordinates: List<List<List<Double>>>
)
@Serializable
data class IsochronesMetadata(
val attribution: String,
val service: String,
val timestamp: Long,
val query: IsochronesQuery,
val engine: IsochronesEngine
)
@Serializable
data class IsochronesQuery(
val profile: String,
val profileName: String,
val locations: List<List<Double>>,
val range: List<Double>, // <-- was List<Int>
@SerialName("range_type") val rangeType: String? = null
)
@Serializable
data class IsochronesEngine(
val version: String,
@SerialName("build_date") val buildDate: String,
@SerialName("graph_date") val graphDate: String,
@SerialName("osm_date") val osmDate: String
)

View File

@@ -1,58 +0,0 @@
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,49 +0,0 @@
package org.nitri.ors.domain.matrix
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents a request for the matrix service.
*
* @property locations A list of coordinates as `[longitude, latitude]` pairs.
* @property destinations Optional. A list of indices that refers to the list of locations (starting with `0`).
* `{ "locations": [[9.70093,48.477473],[9.207916,49.153868],[37.573242,55.801281],[115.663757,38.106467]], "destinations": [0,1] }`
* will calculate distances from origin 0 to destinations 0 and 1, and from origin 1 to destinations 0 and 1.
* Default is all locations.
* @property id Optional. An arbitrary identification string of the request.
* This field is not used by the service and only returned in the response.
* @property metrics Optional. Specifies a list of returned metrics.
* - `distance` - Returns distance matrix in meters.
* - `duration` - Returns duration matrix in seconds.
* Default is `["duration"]`.
* @property resolveLocations Optional. Specifies whether given locations are resolved or not. If `true`, locations are resolved to the road network,
* if `false` just the locations are used.
* Default is `false`.
* @property sources Optional. A list of indices that refers to the list of locations (starting with `0`).
* `{ "locations": [[9.70093,48.477473],[9.207916,49.153868],[37.573242,55.801281],[115.663757,38.106467]], "sources": [0,1] }`
* will calculate distances from origin 0 to all destinations and from origin 1 to all destinations.
* Default is all locations.
*/
@Serializable
data class MatrixRequest(
/** A list of coordinates as `[longitude, latitude]` pairs. */
val locations: List<List<Double>>,
/** Optional. A list of indices that refers to the list of locations (starting with `0`). */
val destinations: List<Int>? = null,
/** Optional. An arbitrary identification string of the request. */
val id: String? = null,
/** Optional. Specifies a list of returned metrics. */
val metrics: List<String>? = null,
@SerialName("resolve_locations")
/** Optional. Specifies whether given locations are resolved or not. */
val resolveLocations: Boolean? = null,
/** Optional. A list of indices that refers to the list of locations (starting with `0`). */
val sources: List<Int>? = null
)

View File

@@ -1,59 +0,0 @@
package org.nitri.ors.domain.matrix
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.nitri.ors.domain.meta.Metadata
/**
* Represents the response from an ORS Matrix API request.
*
* The matrix response contains information about travel times or distances
* between a set of source and destination points.
*
* @property durations A list of lists representing the travel duration matrix.
* Present only if 'durations' was requested in the `metrics` parameter.
* The outer list corresponds to sources, and the inner list corresponds to destinations.
* Each value is the duration in seconds.
* @property distances A list of lists representing the travel distance matrix.
* Present only if 'distances' was requested in the `metrics` parameter.
* The outer list corresponds to sources, and the inner list corresponds to destinations.
* Each value is the distance in meters.
* @property sources A list of enriched waypoint information for the source locations.
* This is typically present when `resolve_locations=true` is used in the request,
* but might sometimes be included even if `resolve_locations=false`.
* @property destinations A list of enriched waypoint information for the destination locations.
* This is typically present when `resolve_locations=true` is used in the request,
* but might sometimes be included even if `resolve_locations=false`.
* @property metadata Standard ORS metadata including information about the service, engine, query, etc.
*/
@Serializable
data class MatrixResponse(
// Present only if requested in `metrics`
val durations: List<List<Double>>? = null,
val distances: List<List<Double>>? = null,
// Enriched waypoint info when resolve_locations=true (or sometimes even if false)
val sources: List<MatrixWaypoint>? = null,
val destinations: List<MatrixWaypoint>? = null,
// Standard ORS metadata (service, engine, query, etc.)
val metadata: Metadata? = null
)
/**
* Represents a waypoint used in the ORS Matrix API response, providing details about a specific location.
*
* @property location The coordinates of the waypoint as a list of [longitude, latitude].
* @property name An optional name associated with the waypoint, which ORS may include.
* @property snappedDistance The distance in meters from the input coordinate to the snapped network coordinate.
* This is an optional extra that ORS may include.
*/
@Serializable
data class MatrixWaypoint(
// [lon, lat]
val location: List<Double>,
// Optional extras ORS may include
val name: String? = null,
@SerialName("snapped_distance")
val snappedDistance: Double? = null
)

View File

@@ -1,26 +0,0 @@
package org.nitri.ors.domain.meta
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@Serializable
data class Metadata(
val id: String? = null,
val attribution: String,
val service: String,
val timestamp: Long,
val query: JsonElement, // could be String, Map<String, JsonElement>, or a specific model
val engine: Engine,
@SerialName("system_message")
val systemMessage: String? = null
)
@Serializable
data class Engine(
val version: String,
@SerialName("build_date")
val buildDate: String,
@SerialName("graph_date")
val graphDate: String
)

View File

@@ -1,86 +0,0 @@
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,162 +0,0 @@
package org.nitri.ors.domain.optimization
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
/**
* Root payload sent to /optimization
* See: https://github.com/VROOM-Project/vroom/blob/master/docs/API.md
*/
@Serializable
data class OptimizationRequest(
/** Required: either jobs and/or shipments, and vehicles */
val jobs: List<Job>? = null,
val shipments: List<Shipment>? = null,
val vehicles: List<Vehicle>,
/** Optional custom matrices keyed by routing profile (e.g. "driving-car") */
val matrices: Map<String, CustomMatrix>? = null,
/** Optional free-form options bag passed to the optimization engine */
val options: Map<String, JsonElement>? = null
)
/* ---------------------------- Basics & helpers ---------------------------- */
@Serializable
data class TimeWindow(
/** seconds since midnight */
val start: Int,
/** seconds since midnight */
val end: Int
)
typealias LonLat = List<Double> // [lon, lat]
typealias Amount = List<Int> // capacities/quantities
typealias Skills = List<Int> // integers as in VROOM API
/* --------------------------------- Jobs ---------------------------------- */
@Serializable
data class Job(
/** Unique job id (required) */
val id: Int,
/** Service time in seconds spent on site (optional) */
val service: Int? = null,
/** Demand to be delivered/picked as a quantity vector (optional) */
val delivery: Amount? = null,
val pickup: Amount? = null,
val amount: Amount? = null, // generic synonym supported by VROOM
/** Location as coordinates or index into the matrix, provide one of them */
val location: LonLat? = null,
@SerialName("location_index") val locationIndex: Int? = null,
/** Allowed vehicles list (restrict which vehicles may handle this job) */
@SerialName("allowed_vehicles") val allowedVehicles: List<Int>? = null,
/** Disallowed vehicles list */
@SerialName("disallowed_vehicles") val disallowedVehicles: List<Int>? = null,
/** Skills required for this job */
val skills: Skills? = null,
/** 0..100 (higher = more important) */
val priority: Int? = null,
/** Multiple time windows supported */
@SerialName("time_windows") val timeWindows: List<TimeWindow>? = null
)
/* ------------------------------- Shipments -------------------------------- */
@Serializable
data class Shipment(
/** Unique shipment id (required) */
val id: Int,
/** Overall amount carried by the shipment */
val amount: Amount? = null,
/** Pickup and delivery legs (each looks like a job) */
val pickup: ShipmentStep,
val delivery: ShipmentStep,
/** Skills & priority rules apply to the *whole* shipment */
val skills: Skills? = null,
val priority: Int? = null
)
@Serializable
data class ShipmentStep(
/** Service time in seconds at this step */
val service: Int? = null,
/** Coordinates or index, provide one of them */
val location: LonLat? = null,
@SerialName("location_index") val locationIndex: Int? = null,
/** Time windows at this step */
@SerialName("time_windows") val timeWindows: List<TimeWindow>? = null
)
/* -------------------------------- Vehicles -------------------------------- */
@Serializable
data class Vehicle(
/** Unique vehicle id (required) */
val id: Int,
/** Routing profile, e.g., "driving-car" (required) */
val profile: String,
/** Vehicle capacity vector */
val capacity: Amount? = null,
/** Start/end positions as coordinates or as matrix indices (pick one style) */
val start: LonLat? = null,
val end: LonLat? = null,
@SerialName("start_index") val startIndex: Int? = null,
@SerialName("end_index") val endIndex: Int? = null,
/** Skills the vehicle provides */
val skills: Skills? = null,
/** When the vehicle is available to operate */
@SerialName("time_window") val timeWindow: TimeWindow? = null,
/** Optional list of breaks (each with service & time windows) */
val breaks: List<VehicleBreak>? = null,
/** Earliest/latest start/end (advanced; seconds since midnight) */
@SerialName("earliest_start") val earliestStart: Int? = null,
@SerialName("latest_end") val latestEnd: Int? = null,
/** Max tasks or max travel time limits (seconds) */
@SerialName("max_tasks") val maxTasks: Int? = null,
@SerialName("max_travel_time") val maxTravelTime: Int? = null,
/** Optional arbitrary metadata to round-trip through the solver */
val description: String? = null
)
@Serializable
data class VehicleBreak(
/** Service time in seconds spent for the break */
val service: Int? = null,
/** Allowed time windows for this break */
@SerialName("time_windows") val timeWindows: List<TimeWindow>
)
/* ------------------------------ Custom matrix ----------------------------- */
@Serializable
data class CustomMatrix(
/** Square matrix in seconds; required if provided */
val durations: List<List<Int>>? = null,
/** Optional distances in meters */
val distances: List<List<Int>>? = null
)

View File

@@ -1,112 +0,0 @@
package org.nitri.ors.domain.optimization
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/** Top-level result from /optimization */
@Serializable
data class OptimizationResponse(
val code: Int,
val summary: OptimizationSummary,
val unassigned: List<Unassigned> = emptyList(),
val routes: List<OptimizedRoute> = emptyList()
)
/* -------------------------------- Summary -------------------------------- */
@Serializable
data class OptimizationSummary(
val cost: Int,
val routes: Int,
val unassigned: Int,
/** Totals present depending on your payload/constraints */
val delivery: List<Int>? = null,
val amount: List<Int>? = null,
val pickup: List<Int>? = null,
val setup: Int,
val service: Int,
val duration: Int,
@SerialName("waiting_time") val waitingTime: Int,
val priority: Int,
/** Usually empty unless you use hard constraints */
val violations: List<Violation> = emptyList(),
@SerialName("computing_times") val computingTimes: ComputingTimes? = null
)
@Serializable
data class ComputingTimes(
val loading: Int? = null,
val solving: Int? = null,
val routing: Int? = null
)
/* ------------------------------ Unassigned ------------------------------- */
@Serializable
data class Unassigned(
val id: Int,
val location: LonLat,
/** e.g. "job" */
val type: String
)
/* --------------------------------- Routes -------------------------------- */
@Serializable
data class OptimizedRoute(
val vehicle: Int,
val cost: Int,
/** Per-route totals (optional keys depending on your model) */
val delivery: List<Int>? = null,
val amount: List<Int>? = null,
val pickup: List<Int>? = null,
val setup: Int,
val service: Int,
val duration: Int,
@SerialName("waiting_time") val waitingTime: Int,
val priority: Int,
val steps: List<RouteStep> = emptyList(),
val violations: List<Violation> = emptyList()
)
@Serializable
data class RouteStep(
/** "start" | "job" | "end" | … */
val type: String,
val location: LonLat,
/** Only for job-like steps */
val id: Int? = null,
val job: Int? = null,
val setup: Int? = null,
val service: Int? = null,
@SerialName("waiting_time") val waitingTime: Int? = null,
/** Vehicle load after performing this step */
val load: List<Int>? = null,
/** Timestamps & cumulated travel */
val arrival: Int? = null,
val duration: Int? = null,
val violations: List<Violation> = emptyList()
)
/* ------------------------------- Violations ------------------------------ */
/** Kept flexible; VROOM may return several shapes depending on constraint hit. */
@Serializable
data class Violation(
val type: String? = null, // e.g., "capacity", "skills", "time_window", …
val id: Int? = null, // job/shipment id if applicable
val job: Int? = null,
val description: String? = null
)

View File

@@ -1,79 +0,0 @@
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

@@ -1,31 +0,0 @@
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,83 +0,0 @@
package org.nitri.ors.domain.pois
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Top-level POIs GeoJSON response:
* {
* "type": "FeatureCollection",
* "bbox": [minLon, minLat, maxLon, maxLat],
* "features": [...],
* "information": {...}
* }
*/
@Serializable
data class PoisGeoJsonResponse(
val type: String, // "FeatureCollection"
val bbox: List<Double>? = null, // [minLon, minLat, maxLon, maxLat]
val features: List<PoiFeature>,
val information: PoisInformation? = null // metadata (sometimes called "information")
)
/** A single GeoJSON feature (Point) */
@Serializable
data class PoiFeature(
val type: String, // "Feature"
val geometry: PoiGeometry,
val properties: PoiProperties
)
@Serializable
data class PoiGeometry(
val type: String, // "Point"
val coordinates: List<Double> // [lon, lat]
)
/**
* Properties seen in your sample. Some fields are optional across results.
* - category_ids is an object with integer keys -> map to Map<Int, CategoryInfo>
* - osm_tags is a free-form OSM tag map (strings)
*/
@Serializable
data class PoiProperties(
@SerialName("osm_id") val osmId: Long,
@SerialName("osm_type") val osmType: Int,
val distance: Double,
@SerialName("category_ids") val categoryIds: Map<Int, CategoryInfo>? = null,
@SerialName("osm_tags") val osmTags: Map<String, String>? = null
)
@Serializable
data class CategoryInfo(
@SerialName("category_name") val categoryName: String,
@SerialName("category_group") val categoryGroup: String
)
/**
* The “information” block at the end of the response.
* Note: ORS uses "information" (not "metadata") here.
*/
@Serializable
data class PoisInformation(
val attribution: String? = null,
val version: String? = null,
val timestamp: Long? = null,
val query: PoisQueryInfo? = null
)
/** Echo of the request inside the information block */
@Serializable
data class PoisQueryInfo(
val request: String? = null, // "pois"
val geometry: PoisQueryGeometry? = null
)
/** Mirrors the requests geometry wrapper */
@Serializable
data class PoisQueryGeometry(
val bbox: List<List<Double>>? = null, // [[minLon,minLat],[maxLon,maxLat]]
val geojson: GeoJsonGeometry? = null,
val buffer: Int? = null
)

View File

@@ -1,23 +0,0 @@
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,28 +0,0 @@
package org.nitri.ors.domain.route
import kotlinx.serialization.Serializable
import org.nitri.ors.domain.meta.Metadata
/** GeoJSON response for the directions endpoint. */
@Serializable
data class GeoJsonRouteResponse(
val type: String,
val bbox: List<Double>,
val features: List<Feature>,
val metadata: Metadata
)
/** A single GeoJSON feature within the route response. */
@Serializable
data class Feature(
val type: String,
val geometry: Geometry,
val properties: Map<String, kotlinx.serialization.json.JsonElement> = emptyMap()
)
/** Geometry of a route feature. */
@Serializable
data class Geometry(
val type: String,
val coordinates: List<List<Double>> // or List<List<List<Double>>> for MultiLineString, etc.
)

View File

@@ -1,33 +0,0 @@
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,
val routes: List<JsonElement> = emptyList(), // route content can vary
val extensions: JsonElement? = null,
val gpxRouteElements: List<JsonElement> = emptyList()
)
@Serializable
data class GpxMetadata(
val name: String? = null,
val description: String? = null,
val author: JsonElement? = null,
val copyright: JsonElement? = null,
val timeGenerated: String? = null,
val bounds: GpxBounds? = null,
val extensions: JsonElement? = null
)
@Serializable
data class GpxBounds(
val asArray: List<Double>? = null,
val minLon: Double,
val maxLon: Double,
val minLat: Double,
val maxLat: Double
)

View File

@@ -1,102 +0,0 @@
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
)
@Serializable
data class AvoidPolygons(
val type: String,
val coordinates: List<List<List<Double>>>
)
@Serializable
data class ProfileParams(
val weightings: Weightings? = null,
val restrictions: Restrictions? = null
)
@Serializable
data class Weightings(
val green: WeightingFactor? = null,
val quiet: WeightingFactor? = null,
val shortest: WeightingFactor? = null
)
@Serializable
data class WeightingFactor(
val factor: Double
)
@Serializable
data class Restrictions(
@SerialName("max_height")
val maxHeight: Double? = null,
@SerialName("max_width")
val maxWidth: Double? = null,
@SerialName("max_weight")
val maxWeight: Double? = null,
@SerialName("max_length")
val maxLength: Double? = null
)

View File

@@ -1,22 +0,0 @@
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,60 +0,0 @@
package org.nitri.ors.domain.route
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.nitri.ors.domain.meta.Metadata
/** Response from the directions endpoint. */
@Serializable
data class RouteResponse(
val routes: List<Route>,
val bbox: List<Double>,
val metadata: Metadata? = null
)
/** Single route variant returned by the API. */
@Serializable
data class Route(
val summary: RouteSummary,
val segments: List<Segment>,
val geometry: String? = null,
@SerialName("way_points")
val wayPoints: List<Int>? = null
)
/** Aggregated summary of a route. */
@Serializable
data class RouteSummary(
val distance: Double,
val duration: Double,
val ascent: Double? = null,
val descent: Double? = null
)
/** One segment between intermediate waypoints. */
@Serializable
data class Segment(
val distance: Double,
val duration: Double,
val steps: List<Step>? = null,
val ascent: Double? = null,
val descent: Double? = null,
@SerialName("detour_factor")
val detourFactor: Double? = null,
val percentage: Double? = null
)
/** Turn instruction within a segment. */
@Serializable
data class Step(
val distance: Double,
val duration: Double,
val instruction: String,
val name: String,
val type: Int,
@SerialName("way_points")
val wayPoints: List<Int>,
@SerialName("exit_number")
val exitNumber: Int? = null
)

View File

@@ -1,38 +0,0 @@
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,43 +0,0 @@
package org.nitri.ors.domain.snap
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.nitri.ors.domain.meta.Metadata
/**
* Snap response in GeoJSON format
*/
@Serializable
data class SnapGeoJsonResponse(
val type: String, // "FeatureCollection"
val features: List<SnapFeature>,
val metadata: Metadata,
val bbox: List<Double>? = null
)
/**
* A GeoJSON Feature with snapped location info
*/
@Serializable
data class SnapFeature(
val type: String, // "Feature"
val properties: SnapProperties,
val geometry: SnapGeometry
)
@Serializable
data class SnapProperties(
val name: String? = null,
@SerialName("snapped_distance")
val snappedDistance: Double,
@SerialName("source_id")
val sourceId: Int
)
@Serializable
data class SnapGeometry(
val type: String, // "Point"
val coordinates: List<Double> // [lon, lat]
)

View File

@@ -1,17 +0,0 @@
package org.nitri.ors.domain.snap
import kotlinx.serialization.Serializable
/**
* Snap request for ORS /v2/snap/{profile}/json
*
* @param locations List of [lon, lat] coordinates to snap.
* @param radius Maximum radius (meters) around given coordinates to search for graph edges.
* @param id Optional client-provided identifier.
*/
@Serializable
data class SnapRequest(
val locations: List<List<Double>>,
val radius: Int,
val id: String? = null
)

View File

@@ -1,30 +0,0 @@
package org.nitri.ors.domain.snap
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.nitri.ors.domain.meta.Metadata
/**
* Response for ORS /v2/snap/{profile}[/json]
*/
@Serializable
data class SnapResponse(
val locations: List<SnapLocation>,
val metadata: Metadata
)
/**
* One snapped input coordinate.
*/
@Serializable
data class SnapLocation(
/** [lon, lat] of the snapped position */
val location: List<Double>,
/** Optional street name if available */
val name: String? = null,
/** Distance in meters from the input to the snapped position */
@SerialName("snapped_distance")
val snappedDistance: Double
)

View File

@@ -1,66 +0,0 @@
package org.nitri.ors.helper
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
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
/** 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 OrsClient.getElevationLine(
coordinates: List<List<Double>>, // [[lon,lat], [lon,lat], ...]
dataset: String? = null,
formatOut: String = "geojson",
): ElevationLineResponse {
val geometry: JsonElement = JsonObject(
mapOf(
"type" to JsonPrimitive("LineString"),
"coordinates" to JsonArray(
coordinates.map { pair ->
JsonArray(pair.map { JsonPrimitive(it) })
}
)
)
)
val request = ElevationLineRequest(
formatIn = "geojson",
formatOut = formatOut,
geometry = geometry,
dataset = dataset
)
return getElevationLine(request)
}
/**
* Calls the ORS elevation/point POST endpoint for a single coordinate.
*
* @param lon longitude
* @param lat latitude
* @param formatOut either "geojson" or "point"
* @param dataset optional dataset (e.g., "srtm")
*/
suspend fun OrsClient.getElevationPoint(
lon: Double,
lat: Double,
formatOut: String = "geojson",
dataset: String? = null,
): ElevationPointResponse {
val request = ElevationPointRequest(
formatIn = "point",
formatOut = formatOut,
dataset = dataset,
geometry = listOf(lon, lat)
)
return getElevationPoint(request)
}
}

View File

@@ -1,41 +0,0 @@
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,170 +0,0 @@
package org.nitri.ors.helper
import org.nitri.ors.OrsClient
import org.nitri.ors.domain.geocode.GeocodeSearchResponse
/**
* Repository for ORS Geocoding endpoints using GET requests only.
*
* Methods are member extensions on [OrsClient] and delegate to it.
*/
class GeocodeHelper {
/**
* Forward geocoding search.
*/
suspend fun OrsClient.search(
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 {
return geocodeSearch(
text = text,
apiKey = apiKey,
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
)
}
/**
* Autocomplete search; returns suggestions for a partial query.
*/
suspend fun OrsClient.autocomplete(
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 {
return geocodeAutocomplete(
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
)
}
/**
* Structured forward geocoding using address fields.
*/
suspend fun OrsClient.structured(
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 {
return 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
)
}
/**
* Reverse geocoding for a point.
*/
suspend fun OrsClient.reverse(
apiKey: String,
lon: Double,
lat: Double,
radiusKm: Double? = null,
size: Int? = null,
layers: List<String>? = null,
sources: List<String>? = null,
boundaryCountry: String? = null,
): GeocodeSearchResponse {
return geocodeReverse(
apiKey = apiKey,
lon = lon,
lat = lat,
radiusKm = radiusKm,
size = size,
layers = layers,
sources = sources,
boundaryCountry = boundaryCountry
)
}
}

View File

@@ -1,24 +0,0 @@
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

@@ -1,31 +0,0 @@
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

@@ -1,36 +0,0 @@
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,61 +0,0 @@
package org.nitri.ors.helper
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
/** Convenience extensions for the POIs endpoints. */
class PoisHelper {
/**
* Query POIs within a bounding box.
*
* @param bbox [[minLon,minLat],[maxLon,maxLat]]
* @param filters Optional filters map as supported by ORS POIs
* @param limit Optional limit for number of features returned
* @param sortby Optional sort field
* @param buffer Optional buffer in meters applied to the geometry
*/
suspend fun OrsClient.getPoisByBbox(
bbox: List<List<Double>>,
filters: Map<String, String>? = null,
limit: Int? = null,
sortby: String? = null,
buffer: Int? = null
): PoisGeoJsonResponse {
val request = PoisRequest(
geometry = Geometry(bbox = bbox, buffer = buffer),
filters = filters,
limit = limit,
sortby = sortby
)
return getPois(request)
}
/**
* Query POIs around a point with a buffer radius.
*
* @param point [lon, lat]
* @param buffer Buffer radius in meters
*/
suspend fun OrsClient.getPoisByPoint(
point: List<Double>,
buffer: Int,
filters: Map<String, String>? = null,
limit: Int? = null,
sortby: String? = null
): PoisGeoJsonResponse {
val request = PoisRequest(
geometry = Geometry(
geojson = GeoJsonGeometry(type = "Point", coordinates = point),
buffer = buffer
),
filters = filters,
limit = limit,
sortby = sortby
)
return getPois(request)
}
}

View File

@@ -1,81 +0,0 @@
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

@@ -1,56 +0,0 @@
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,96 +0,0 @@
package org.nitri.ors.restclient
import android.content.Context
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.create
import org.nitri.ors.api.OpenRouteServiceApi
import okhttp3.MediaType.Companion.toMediaType
import java.util.concurrent.TimeUnit
/**
* 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
isLenient = true
allowSpecialFloatingPointValues = true
}
val client = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.addInterceptor { chain ->
val original = chain.request()
val newRequest = original.newBuilder()
// ORS expects the API key in the Authorization header (no Bearer prefix)
.addHeader("Authorization", apiKey)
.addHeader("User-Agent", userAgent)
.addHeader("Accept", "application/json, application/geo+json, application/xml, text/xml, application/gpx+xml")
.build()
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)
.writeTimeout(60, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.openrouteservice.org/")
.client(client)
// Prefer application/json for requests; also support application/geo+json responses
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.addConverterFactory(json.asConverterFactory("application/geo+json".toMediaType()))
.build()
return retrofit.create()
}
}

View File

@@ -1,49 +0,0 @@
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,57 +0,0 @@
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.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 RouteHelperTest {
private lateinit var client: OrsClient
private lateinit var routeHelper: RouteHelper
@Before
fun setUp() {
client = mock(OrsClient::class.java)
routeHelper = RouteHelper()
}
@Test
fun `getRoute returns response from API`() = runTest {
val start = Pair(8.681495, 49.41461)
val end = Pair(8.687872, 49.420318)
val profile = "driving-car"
val expectedResponse = RouteResponse(
routes = listOf(
Route(
summary = RouteSummary(1000.0, 600.0),
geometry = "encodedPolyline",
segments = emptyList()
)
),
bbox = listOf(0.0, 0.0, 1.0, 1.0)
)
val expectedRequest = RouteRequest(
coordinates = listOf(
listOf(start.first, start.second),
listOf(end.first, end.second)
)
)
`when`(client.getRoute(Profile.DRIVING_CAR, expectedRequest)).thenReturn(expectedResponse)
val result = with(routeHelper) { client.getRoute(start, end, profile) }
assert(result == expectedResponse)
verify(client).getRoute(Profile.DRIVING_CAR, expectedRequest)
}
}

View File

@@ -1,33 +0,0 @@
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

@@ -1,35 +0,0 @@
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()
}
}
}

View File

@@ -6,4 +6,4 @@ pluginManagement {
}
}
include(":app", ":ors-client")
include(":app")