Skip to content

Commit 65dd5c0

Browse files
committed
Merge branch “feature-ors”
2 parents fe4faeb + 206b36d commit 65dd5c0

17 files changed

+937
-3
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.nitri.ors
2+
3+
import android.content.Context
4+
import androidx.test.core.app.ApplicationProvider
5+
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import kotlinx.coroutines.runBlocking
7+
import org.junit.Assert.assertNotNull
8+
import org.junit.Assert.assertTrue
9+
import org.junit.Test
10+
import org.junit.runner.RunWith
11+
import org.nitri.ors.client.OpenRouteServiceClient
12+
import org.nitri.ors.model.optimization.Job
13+
import org.nitri.ors.model.optimization.Vehicle
14+
import org.nitri.ors.repository.OptimizationRepository
15+
16+
@RunWith(AndroidJUnit4::class)
17+
class OptimizationInstrumentedTest {
18+
19+
private fun createRepository(context: Context): OptimizationRepository {
20+
val apiKey = context.getString(R.string.ors_api_key)
21+
val api = OpenRouteServiceClient.create(apiKey, context)
22+
return OptimizationRepository(api)
23+
}
24+
25+
@Test
26+
fun testOptimization_successful() = runBlocking {
27+
val context = ApplicationProvider.getApplicationContext<Context>()
28+
val repository = createRepository(context)
29+
30+
// Simple scenario in/near Heidelberg, Germany
31+
val vehicle = Vehicle(
32+
id = 1,
33+
profile = "driving-car",
34+
// Start and end at/near Heidelberg castle parking
35+
start = listOf(8.6910, 49.4100),
36+
end = listOf(8.6910, 49.4100)
37+
)
38+
39+
val jobs = listOf(
40+
Job(
41+
id = 101,
42+
location = listOf(8.681495, 49.41461) // Heidelberg center
43+
),
44+
Job(
45+
id = 102,
46+
location = listOf(8.687872, 49.420318) // Nearby point
47+
)
48+
)
49+
50+
val response = repository.getOptimization(
51+
vehicles = listOf(vehicle),
52+
jobs = jobs
53+
)
54+
55+
// Basic assertions
56+
assertNotNull("Optimization response should not be null", response)
57+
assertNotNull("Summary should be present", response.summary)
58+
assertTrue("Routes list should be present", response.routes != null)
59+
assertTrue("Routes should not be empty for solvable small case", response.routes.isNotEmpty())
60+
61+
val firstRoute = response.routes.first()
62+
assertTrue("Route steps should not be empty", firstRoute.steps.isNotEmpty())
63+
// Code is typically 0 for success in VROOM-like APIs; ensure non-negative as a safe check
64+
assertTrue("Response code should be non-negative", response.code >= 0)
65+
66+
// Optional additional sanity checks
67+
assertTrue("Total duration should be >= 0", response.summary.duration >= 0)
68+
assertTrue("Total service should be >= 0", response.summary.service >= 0)
69+
}
70+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.nitri.ors
2+
3+
import android.content.Context
4+
import androidx.test.core.app.ApplicationProvider
5+
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import kotlinx.coroutines.runBlocking
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Assert.assertNotNull
9+
import org.junit.Assert.assertTrue
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
import org.nitri.ors.client.OpenRouteServiceClient
13+
import org.nitri.ors.repository.PoisRepository
14+
15+
@RunWith(AndroidJUnit4::class)
16+
class PoisInstrumentedTest {
17+
18+
private fun createRepository(context: Context): PoisRepository {
19+
val apiKey = context.getString(R.string.ors_api_key)
20+
val api = OpenRouteServiceClient.create(apiKey, context)
21+
return PoisRepository(api)
22+
}
23+
24+
@Test
25+
fun testPois_byBbox_successful() = runBlocking {
26+
val context = ApplicationProvider.getApplicationContext<Context>()
27+
val repository = createRepository(context)
28+
29+
// Bounding box around Heidelberg, Germany
30+
val bbox = listOf(
31+
listOf(8.67, 49.40), // minLon, minLat
32+
listOf(8.70, 49.43) // maxLon, maxLat
33+
)
34+
35+
val response = repository.getPoisByBbox(
36+
bbox = bbox,
37+
limit = 10
38+
)
39+
40+
assertNotNull("POIs response should not be null", response)
41+
assertEquals("GeoJSON type should be FeatureCollection", "FeatureCollection", response.type)
42+
assertNotNull("Information block should be present", response.information)
43+
assertTrue("Features should not be empty in a city bbox", response.features.isNotEmpty())
44+
45+
val first = response.features.first()
46+
assertEquals("Feature type should be Feature", "Feature", first.type)
47+
assertEquals("Geometry type should be Point", "Point", first.geometry.type)
48+
assertEquals("Point coordinates should be [lon, lat]", 2, first.geometry.coordinates.size)
49+
50+
// Basic properties sanity
51+
val props = first.properties
52+
assertTrue("OSM id should be positive", props.osmId > 0)
53+
assertTrue("Distance should be non-negative", props.distance >= 0.0)
54+
}
55+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package org.nitri.ors
2+
3+
import android.content.Context
4+
import androidx.test.core.app.ApplicationProvider
5+
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import kotlinx.coroutines.runBlocking
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Assert.assertNotNull
9+
import org.junit.Assert.assertTrue
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
import org.nitri.ors.client.OpenRouteServiceClient
13+
import org.nitri.ors.repository.SnapRepository
14+
15+
@RunWith(AndroidJUnit4::class)
16+
class SnapInstrumentedTest {
17+
18+
private fun createRepository(context: Context): SnapRepository {
19+
val apiKey = context.getString(R.string.ors_api_key)
20+
val api = OpenRouteServiceClient.create(apiKey, context)
21+
return SnapRepository(api)
22+
}
23+
24+
@Test
25+
fun testSnap_successful() = runBlocking {
26+
val context = ApplicationProvider.getApplicationContext<Context>()
27+
val repository = createRepository(context)
28+
29+
// Two locations in/near Heidelberg, Germany [lon, lat]
30+
val locations = listOf(
31+
listOf(8.681495, 49.41461), // Heidelberg center
32+
listOf(8.687872, 49.420318) // Nearby point
33+
)
34+
val profile = "driving-car"
35+
val radius = 50 // meters
36+
37+
val response = repository.getSnap(
38+
locations = locations,
39+
radius = radius,
40+
profile = profile,
41+
id = "snap_test"
42+
)
43+
44+
assertNotNull("Snap response should not be null", response)
45+
assertNotNull("Metadata should be present", response.metadata)
46+
assertTrue("Locations should not be empty", response.locations.isNotEmpty())
47+
assertEquals("Should have as many results as inputs", locations.size, response.locations.size)
48+
49+
val first = response.locations.first()
50+
assertNotNull("First snapped location should have coordinates", first.location)
51+
assertEquals("Snapped coordinates should have 2 values [lon, lat]", 2, first.location.size)
52+
assertTrue("Snapped distance should be non-negative", first.snappedDistance >= 0.0)
53+
}
54+
55+
@Test
56+
fun testSnapJson_successful() = runBlocking {
57+
val context = ApplicationProvider.getApplicationContext<Context>()
58+
val repository = createRepository(context)
59+
60+
val locations = listOf(
61+
listOf(8.681495, 49.41461),
62+
listOf(8.687872, 49.420318)
63+
)
64+
val profile = "driving-car"
65+
val radius = 50
66+
67+
val response = repository.getSnapJson(
68+
locations = locations,
69+
radius = radius,
70+
profile = profile,
71+
id = "snap_json_test"
72+
)
73+
74+
assertNotNull("Snap JSON response should not be null", response)
75+
assertNotNull("Metadata should be present", response.metadata)
76+
assertTrue("Locations should not be empty", response.locations.isNotEmpty())
77+
response.locations.forEach { loc ->
78+
assertEquals("Coordinate should be [lon, lat]", 2, loc.location.size)
79+
assertTrue("Snapped distance should be non-negative", loc.snappedDistance >= 0.0)
80+
}
81+
}
82+
83+
@Test
84+
fun testSnapGeoJson_successful() = runBlocking {
85+
val context = ApplicationProvider.getApplicationContext<Context>()
86+
val repository = createRepository(context)
87+
88+
val locations = listOf(
89+
listOf(8.681495, 49.41461),
90+
listOf(8.687872, 49.420318)
91+
)
92+
val profile = "driving-car"
93+
val radius = 50
94+
95+
val response = repository.getSnapGeoJson(
96+
locations = locations,
97+
radius = radius,
98+
profile = profile,
99+
id = "snap_geojson_test"
100+
)
101+
102+
assertNotNull("Snap GeoJSON response should not be null", response)
103+
assertEquals("GeoJSON type should be FeatureCollection", "FeatureCollection", response.type)
104+
assertNotNull("Metadata should be present", response.metadata)
105+
assertTrue("Features should not be empty", response.features.isNotEmpty())
106+
107+
val feature = response.features.first()
108+
assertEquals("Feature type should be Feature", "Feature", feature.type)
109+
assertEquals("Geometry type should be Point", "Point", feature.geometry.type)
110+
assertEquals("Point coordinates should be [lon, lat]", 2, feature.geometry.coordinates.size)
111+
assertTrue("snapped_distance should be non-negative", feature.properties.snappedDistance >= 0.0)
112+
assertTrue("source_id should be non-negative", feature.properties.sourceId >= 0)
113+
}
114+
}

ors-client/src/main/java/org/nitri/ors/api/OpenRouteServiceApi.kt

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package org.nitri.ors.api
22

3-
import org.nitri.ors.model.matrix.MatrixResponse
43
import okhttp3.ResponseBody
54
import org.nitri.ors.model.export.ExportRequest
65
import org.nitri.ors.model.export.ExportResponse
76
import org.nitri.ors.model.export.TopoJsonExportResponse
87
import org.nitri.ors.model.isochrones.IsochronesRequest
98
import org.nitri.ors.model.isochrones.IsochronesResponse
109
import org.nitri.ors.model.matrix.MatrixRequest
10+
import org.nitri.ors.model.matrix.MatrixResponse
11+
import org.nitri.ors.model.optimization.OptimizationRequest
12+
import org.nitri.ors.model.optimization.OptimizationResponse
13+
import org.nitri.ors.model.pois.PoisGeoJsonResponse
14+
import org.nitri.ors.model.pois.PoisRequest
1115
import org.nitri.ors.model.route.GeoJsonRouteResponse
1216
import org.nitri.ors.model.route.RouteRequest
1317
import org.nitri.ors.model.route.RouteResponse
18+
import org.nitri.ors.model.snap.SnapGeoJsonResponse
19+
import org.nitri.ors.model.snap.SnapRequest
20+
import org.nitri.ors.model.snap.SnapResponse
1421
import retrofit2.Response
1522
import retrofit2.http.Body
1623
import retrofit2.http.GET
@@ -20,6 +27,8 @@ import retrofit2.http.Query
2027

2128
interface OpenRouteServiceApi {
2229

30+
// Directions
31+
2332
@GET("v2/directions/{profile}")
2433
suspend fun getRouteSimple(
2534
@Path("profile") profile: String,
@@ -84,5 +93,38 @@ interface OpenRouteServiceApi {
8493
@Body request: MatrixRequest
8594
): MatrixResponse
8695

87-
96+
// Snapping
97+
98+
@POST("v2/snap/{profile}")
99+
suspend fun getSnap(
100+
@Path("profile") profile: String,
101+
@Body request: SnapRequest
102+
): SnapResponse
103+
104+
@POST("v2/snap/{profile}/json")
105+
suspend fun getSnapJson(
106+
@Path("profile") profile: String,
107+
@Body request: SnapRequest
108+
): SnapResponse
109+
110+
@POST("v2/snap/{profile}/geojson")
111+
suspend fun getSnapGeoJson(
112+
@Path("profile") profile: String,
113+
@Body request: SnapRequest
114+
): SnapGeoJsonResponse
115+
116+
// POIs
117+
118+
@POST("pois")
119+
suspend fun getPois(
120+
@Body request: PoisRequest
121+
): PoisGeoJsonResponse
122+
123+
// Optimization
124+
125+
@POST("optimization")
126+
suspend fun getOptimization(
127+
@Body request: OptimizationRequest
128+
): OptimizationResponse
129+
88130
}

ors-client/src/main/java/org/nitri/ors/client/OpenRouteServiceClient.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import retrofit2.Retrofit
99
import retrofit2.create
1010
import org.nitri.ors.api.OpenRouteServiceApi
1111
import okhttp3.MediaType.Companion.toMediaType
12+
import java.util.concurrent.TimeUnit
1213

1314
object OpenRouteServiceClient {
1415
fun create(apiKey: String, context: Context): OpenRouteServiceApi {
@@ -27,18 +28,26 @@ object OpenRouteServiceClient {
2728
val original = chain.request()
2829

2930
val newRequest = original.newBuilder()
30-
.addHeader("Authorization", "Bearer $apiKey")
31+
// ORS expects the API key in the Authorization header (no Bearer prefix)
32+
.addHeader("Authorization", apiKey)
3133
.addHeader("User-Agent", userAgent)
34+
.addHeader("Accept", "application/json, application/geo+json, application/xml, text/xml, application/gpx+xml")
3235
.build()
3336

3437
chain.proceed(newRequest)
3538
}
39+
// Increase timeouts to accommodate heavier endpoints like POIs
40+
.connectTimeout(30, TimeUnit.SECONDS)
41+
.readTimeout(60, TimeUnit.SECONDS)
42+
.writeTimeout(60, TimeUnit.SECONDS)
3643
.build()
3744

3845
val retrofit = Retrofit.Builder()
3946
.baseUrl("https://api.openrouteservice.org/")
4047
.client(client)
48+
// Prefer application/json for requests; also support application/geo+json responses
4149
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
50+
.addConverterFactory(json.asConverterFactory("application/geo+json".toMediaType()))
4251
.build()
4352

4453
return retrofit.create()

0 commit comments

Comments
 (0)