Skip to content

Commit 522cc71

Browse files
committed
Merge branch “feature-ors”
2 parents 65dd5c0 + 3a5e6c1 commit 522cc71

File tree

10 files changed

+783
-0
lines changed

10 files changed

+783
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.ElevationRepository
14+
15+
@RunWith(AndroidJUnit4::class)
16+
class ElevationInstrumentedTest {
17+
18+
private fun createRepository(context: Context): ElevationRepository {
19+
val apiKey = context.getString(R.string.ors_api_key)
20+
val api = OpenRouteServiceClient.create(apiKey, context)
21+
return ElevationRepository(api)
22+
}
23+
24+
@Test
25+
fun testElevation_point_successful() = runBlocking {
26+
val context = ApplicationProvider.getApplicationContext<Context>()
27+
val repository = createRepository(context)
28+
29+
// A point near Heidelberg, Germany
30+
val lon = 8.681495
31+
val lat = 49.41461
32+
33+
val response = repository.getElevationPoint(lon = lon, lat = lat)
34+
35+
assertNotNull("Elevation point response should not be null", response)
36+
assertNotNull("Geometry should not be null", response.geometry)
37+
assertEquals("Geometry type should be Point", "Point", response.geometry.type)
38+
39+
val coords = response.geometry.coordinates
40+
assertTrue("Point coordinates should contain at least [lon, lat]", coords.size >= 2)
41+
// Typically API returns elevation as third value
42+
if (coords.size >= 3) {
43+
// elevation could be any double, just ensure it's a number
44+
val elevation = coords[2]
45+
assertTrue("Elevation should be a finite number", elevation.isFinite())
46+
}
47+
}
48+
49+
@Test
50+
fun testElevation_line_successful() = runBlocking {
51+
val context = ApplicationProvider.getApplicationContext<Context>()
52+
val repository = createRepository(context)
53+
54+
// A short line segment around Heidelberg
55+
val coordinates = listOf(
56+
listOf(8.681495, 49.41461),
57+
listOf(8.687872, 49.420318)
58+
)
59+
60+
val response = repository.getElevationLine(coordinates = coordinates)
61+
62+
assertNotNull("Elevation line response should not be null", response)
63+
assertEquals("Geometry type should be LineString", "LineString", response.geometry.type)
64+
val lineCoords = response.geometry.coordinates
65+
assertTrue("LineString should have at least 2 points", lineCoords.size >= 2)
66+
67+
val first = lineCoords.first()
68+
assertTrue("Each coordinate should have at least [lon, lat]", first.size >= 2)
69+
if (first.size >= 3) {
70+
val elevation = first[2]
71+
assertTrue("Elevation should be a finite number", elevation.isFinite())
72+
}
73+
}
74+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.repository.GeocodeRepository
13+
14+
@RunWith(AndroidJUnit4::class)
15+
class GeocodeInstrumentedTest {
16+
17+
private fun createRepository(context: Context): GeocodeRepository {
18+
val apiKey = context.getString(R.string.ors_api_key)
19+
val api = OpenRouteServiceClient.create(apiKey, context)
20+
return GeocodeRepository(api)
21+
}
22+
23+
@Test
24+
fun testGeocode_search_successful() = runBlocking {
25+
val context = ApplicationProvider.getApplicationContext<Context>()
26+
val repo = createRepository(context)
27+
val apiKey = context.getString(R.string.ors_api_key)
28+
29+
val response = repo.search(
30+
text = "Heidelberg",
31+
apiKey = apiKey,
32+
size = 5
33+
)
34+
35+
assertNotNull("Search response should not be null", response)
36+
assertTrue("Features should not be empty for Heidelberg search", response.features.isNotEmpty())
37+
38+
val first = response.features.first()
39+
// Basic geometry sanity
40+
assertNotNull("First feature geometry should not be null", first.geometry)
41+
val geom = first.geometry!!
42+
assertTrue("Geometry coordinates should have at least [lon, lat]", geom.coordinates.size >= 2)
43+
// Basic properties sanity
44+
assertNotNull("First feature properties should not be null", first.properties)
45+
val name = first.properties?.name ?: first.properties?.label
46+
assertTrue("Feature should have a name or label", !name.isNullOrBlank())
47+
}
48+
49+
@Test
50+
fun testGeocode_reverse_successful() = runBlocking {
51+
val context = ApplicationProvider.getApplicationContext<Context>()
52+
val repo = createRepository(context)
53+
val apiKey = context.getString(R.string.ors_api_key)
54+
55+
// Point near Heidelberg, Germany
56+
val lon = 8.681495
57+
val lat = 49.41461
58+
59+
val response = repo.reverse(
60+
apiKey = apiKey,
61+
lon = lon,
62+
lat = lat,
63+
size = 5
64+
)
65+
66+
assertNotNull("Reverse response should not be null", response)
67+
assertTrue("Reverse should return at least one feature", response.features.isNotEmpty())
68+
val first = response.features.first()
69+
assertNotNull("First reverse feature properties should not be null", first.properties)
70+
val label = first.properties?.label ?: first.properties?.name
71+
assertTrue("Reverse feature should provide a label/name", !label.isNullOrBlank())
72+
}
73+
74+
@Test
75+
fun testGeocode_autocomplete_successful() = runBlocking {
76+
val context = ApplicationProvider.getApplicationContext<Context>()
77+
val repo = createRepository(context)
78+
val apiKey = context.getString(R.string.ors_api_key)
79+
80+
val response = repo.autocomplete(
81+
apiKey = apiKey,
82+
text = "Heidelb",
83+
size = 5
84+
)
85+
86+
assertNotNull("Autocomplete response should not be null", response)
87+
assertTrue("Autocomplete should return suggestions", response.features.isNotEmpty())
88+
}
89+
}

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

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package org.nitri.ors.api
22

33
import okhttp3.ResponseBody
4+
import org.nitri.ors.model.elevation.ElevationLineRequest
5+
import org.nitri.ors.model.elevation.ElevationLineResponse
6+
import org.nitri.ors.model.elevation.ElevationPointRequest
7+
import org.nitri.ors.model.elevation.ElevationPointResponse
48
import org.nitri.ors.model.export.ExportRequest
59
import org.nitri.ors.model.export.ExportResponse
610
import org.nitri.ors.model.export.TopoJsonExportResponse
11+
import org.nitri.ors.model.geocode.GeocodeSearchResponse
712
import org.nitri.ors.model.isochrones.IsochronesRequest
813
import org.nitri.ors.model.isochrones.IsochronesResponse
914
import org.nitri.ors.model.matrix.MatrixRequest
@@ -127,4 +132,131 @@ interface OpenRouteServiceApi {
127132
@Body request: OptimizationRequest
128133
): OptimizationResponse
129134

135+
// Elevation
136+
137+
@POST("elevation/line")
138+
suspend fun getElevationLine(
139+
@Body request: ElevationLineRequest
140+
): ElevationLineResponse
141+
142+
@GET("elevation/point")
143+
suspend fun getElevationPointSimple(
144+
@Query("geometry") start: String, // e.g. "8.681495,49.41461"
145+
): RouteResponse
146+
147+
@POST("elevation/point")
148+
suspend fun getElevationPoint(
149+
@Body request: ElevationPointRequest
150+
): ElevationPointResponse
151+
152+
// Geocode
153+
154+
@GET("geocode/search")
155+
suspend fun geocodeSearch(
156+
@Query("text") text: String,
157+
158+
// Optional focus point
159+
@Query("focus.point.lon") focusLon: Double? = null,
160+
@Query("focus.point.lat") focusLat: Double? = null,
161+
162+
// Optional rectangular boundary
163+
@Query("boundary.rect.min_lon") rectMinLon: Double? = null,
164+
@Query("boundary.rect.min_lat") rectMinLat: Double? = null,
165+
@Query("boundary.rect.max_lon") rectMaxLon: Double? = null,
166+
@Query("boundary.rect.max_lat") rectMaxLat: Double? = null,
167+
168+
// Optional circular boundary
169+
@Query("boundary.circle.lon") circleLon: Double? = null,
170+
@Query("boundary.circle.lat") circleLat: Double? = null,
171+
@Query("boundary.circle.radius") circleRadiusMeters: Double? = null,
172+
173+
// Other optional filters
174+
@Query("boundary.gid") boundaryGid: String? = null,
175+
// Pass comma-separated if multiple, e.g. "DE,AT"
176+
@Query("boundary.country") boundaryCountry: String? = null,
177+
// Pelias expects comma-separated values; join your list before passing.
178+
@Query("sources") sourcesCsv: String? = null, // e.g. "osm,oa,gn,wof"
179+
@Query("layers") layersCsv: String? = null, // e.g. "region,country,locality,address"
180+
@Query("size") size: Int? = 10,
181+
182+
// Geocoder uses api_key as query
183+
@Query("api_key") apiKey: String
184+
): GeocodeSearchResponse
185+
186+
@GET("geocode/autocomplete")
187+
suspend fun autocomplete(
188+
@Query("api_key") apiKey: String,
189+
@Query("text") text: String,
190+
@Query("focus.point.lon") focusLon: Double? = null,
191+
@Query("focus.point.lat") focusLat: Double? = null,
192+
@Query("boundary.rect.min_lon") rectMinLon: Double? = null,
193+
@Query("boundary.rect.min_lat") rectMinLat: Double? = null,
194+
@Query("boundary.rect.max_lon") rectMaxLon: Double? = null,
195+
@Query("boundary.rect.max_lat") rectMaxLat: Double? = null,
196+
@Query("boundary.circle.lon") circleLon: Double? = null,
197+
@Query("boundary.circle.lat") circleLat: Double? = null,
198+
@Query("boundary.circle.radius") circleRadius: Double? = null,
199+
@Query("boundary.country") country: String? = null,
200+
@Query("sources") sources: List<String>? = null,
201+
@Query("layers") layers: List<String>? = null,
202+
@Query("size") size: Int? = null
203+
): GeocodeSearchResponse
204+
205+
206+
@GET("geocode/search/structured")
207+
suspend fun geocodeStructured(
208+
@Query("api_key") apiKey: String,
209+
210+
// Structured query parts (all optional; send only what you have)
211+
@Query("address") address: String? = null,
212+
@Query("neighbourhood") neighbourhood: String? = null,
213+
@Query("borough") borough: String? = null,
214+
@Query("locality") locality: String? = null, // e.g., city
215+
@Query("county") county: String? = null,
216+
@Query("region") region: String? = null, // e.g., state/province
217+
@Query("country") country: String? = null, // ISO code or name
218+
@Query("postalcode") postalcode: String? = null,
219+
220+
// Focus point (used for ranking)
221+
@Query("focus.point.lon") focusLon: Double? = null,
222+
@Query("focus.point.lat") focusLat: Double? = null,
223+
224+
// Bounding rectangle (optional)
225+
@Query("boundary.rect.min_lon") rectMinLon: Double? = null,
226+
@Query("boundary.rect.min_lat") rectMinLat: Double? = null,
227+
@Query("boundary.rect.max_lon") rectMaxLon: Double? = null,
228+
@Query("boundary.rect.max_lat") rectMaxLat: Double? = null,
229+
230+
// Bounding circle (optional)
231+
@Query("boundary.circle.lon") circleLon: Double? = null,
232+
@Query("boundary.circle.lat") circleLat: Double? = null,
233+
@Query("boundary.circle.radius") circleRadiusMeters: Double? = null,
234+
235+
// Limit results to a specific country (ISO code)
236+
@Query("boundary.country") boundaryCountry: String? = null,
237+
238+
// Filters
239+
@Query("layers") layers: List<String>? = null, // e.g. ["address","venue","street","locality","region","country"]
240+
@Query("sources") sources: List<String>? = null, // e.g. ["osm","oa","gn","wof"]
241+
242+
// Number of results (default 10)
243+
@Query("size") size: Int? = null
244+
): GeocodeSearchResponse
245+
246+
247+
@GET("geocode/reverse")
248+
suspend fun geocodeReverse(
249+
@Query("api_key") apiKey: String,
250+
251+
// required
252+
@Query("point.lon") lon: Double,
253+
@Query("point.lat") lat: Double,
254+
255+
// optional ranking/filters
256+
@Query("boundary.circle.radius") radiusKm: Double? = null, // Pelias expects km
257+
@Query("size") size: Int? = null, // default 10
258+
@Query("layers") layers: List<String>? = null, // e.g. ["address","venue"]
259+
@Query("sources") sources: List<String>? = null, // e.g. ["osm","oa","gn","wof"]
260+
@Query("boundary.country") boundaryCountry: String? = null // ISO code, e.g. "FR"
261+
): GeocodeSearchResponse
130262
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.nitri.ors.model.elevation
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.json.JsonElement
6+
7+
8+
@Serializable
9+
data class ElevationLineRequest(
10+
/** One of: "geojson", "polyline", "encodedpolyline5", "encodedpolyline6" */
11+
@SerialName("format_in") val formatIn: String,
12+
13+
/** One of: "geojson", "polyline", "encodedpolyline5", "encodedpolyline6" */
14+
@SerialName("format_out") val formatOut: String,
15+
16+
/**
17+
* Geometry payload. The API accepts different shapes depending on format:
18+
* - format_in = "geojson" -> a GeoJSON LineString object
19+
* - format_in = "polyline" -> [[lon,lat], [lon,lat], ...]
20+
* - format_in = "encodedpolyline5/6"-> a single encoded polyline string
21+
*
22+
* Using JsonElement keeps this field flexible for all variants.
23+
*/
24+
val geometry: JsonElement,
25+
26+
/** Optional: pick a specific elevation dataset (e.g., "SRTM", "COP90", …) */
27+
val dataset: String? = null
28+
)
29+
30+
object ElevationFormats {
31+
const val GEOJSON = "geojson"
32+
const val POLYLINE = "polyline"
33+
const val ENCODED_5 = "encodedpolyline5"
34+
const val ENCODED_6 = "encodedpolyline6"
35+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.nitri.ors.model.elevation
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class ElevationLineResponse(
7+
val attribution: String? = null,
8+
val geometry: ElevationLineGeometry,
9+
val timestamp: Long? = null,
10+
val version: String? = null
11+
)
12+
13+
@Serializable
14+
data class ElevationLineGeometry(
15+
val type: String, // "LineString"
16+
/** Coordinates with elevation: [lon, lat, ele] (API may return [lon,lat] if ele missing) */
17+
val coordinates: List<List<Double>>
18+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.nitri.ors.model.elevation
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class ElevationPointRequest(
8+
@SerialName("format_in")
9+
val formatIn: String, // Input format, must be provided (e.g., "point")
10+
11+
@SerialName("format_out")
12+
val formatOut: String = "geojson", // "geojson" or "point"
13+
14+
val dataset: String? = null, // Optional dataset, e.g. "srtm"
15+
16+
val geometry: List<Double> // [lon, lat]
17+
)

0 commit comments

Comments
 (0)