Skip to content

Commit dd1338e

Browse files
committed
Add isochrones endpoint
1 parent 3eadabb commit dd1338e

File tree

5 files changed

+216
-0
lines changed

5 files changed

+216
-0
lines changed
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.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.IsochronesRepository
13+
14+
@RunWith(AndroidJUnit4::class)
15+
class IsochronesInstrumentedTest {
16+
17+
private fun createRepository(context: Context): IsochronesRepository {
18+
val apiKey = context.getString(R.string.ors_api_key)
19+
val api = OpenRouteServiceClient.create(apiKey, context)
20+
return IsochronesRepository(api)
21+
}
22+
23+
@Test
24+
fun testIsochrones_successful() = runBlocking {
25+
val context = ApplicationProvider.getApplicationContext<Context>()
26+
val repository = createRepository(context)
27+
28+
// Heidelberg, Germany [lon, lat]
29+
val locations = listOf(
30+
listOf(8.681495, 49.41461)
31+
)
32+
// 5 minutes (300 seconds)
33+
val range = listOf(300)
34+
val profile = "driving-car"
35+
36+
val response = repository.getIsochrones(
37+
locations = locations,
38+
range = range,
39+
profile = profile,
40+
attributes = null,
41+
rangeType = "time"
42+
)
43+
44+
assertNotNull("Isochrones response should not be null", response)
45+
assertTrue("Features should not be empty", response.features.isNotEmpty())
46+
47+
val first = response.features.first()
48+
assertNotNull("Feature geometry should not be null", first.geometry)
49+
50+
// Basic metadata sanity
51+
assertTrue("Response type should not be blank", response.type.isNotBlank())
52+
assertNotNull("Metadata should be present", response.metadata)
53+
assertTrue("BBox should have 4 numbers", response.bbox.size == 4)
54+
}
55+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import okhttp3.ResponseBody
44
import org.nitri.ors.model.export.ExportRequest
55
import org.nitri.ors.model.export.ExportResponse
66
import org.nitri.ors.model.export.TopoJsonExportResponse
7+
import org.nitri.ors.model.isochrones.IsochronesRequest
8+
import org.nitri.ors.model.isochrones.IsochronesResponse
79
import org.nitri.ors.model.route.GeoJsonRouteResponse
810
import org.nitri.ors.model.route.RouteRequest
911
import org.nitri.ors.model.route.RouteResponse
@@ -66,5 +68,12 @@ interface OpenRouteServiceApi {
6668
@Body request: ExportRequest
6769
): TopoJsonExportResponse
6870

71+
// Isochrones endpoint
72+
@POST("v2/isochrones/{profile}")
73+
suspend fun getIsochrones(
74+
@Path("profile") profile: String,
75+
@Body request: IsochronesRequest
76+
): IsochronesResponse
77+
6978

7079
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package org.nitri.ors.model.isochrones
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.json.JsonElement
6+
7+
/**
8+
* ORS Isochrones request
9+
* Docs: /v2/isochrones/{profile}
10+
*
11+
* Required: locations, range
12+
* Optional are nullable and omitted from JSON when null.
13+
*/
14+
@Serializable
15+
data class IsochronesRequest(
16+
/** [lon, lat] pairs */
17+
val locations: List<List<Double>>,
18+
19+
/** In seconds for range_type="time" (default) or in meters for "distance". */
20+
val range: List<Int>,
21+
22+
/** "time" (default) or "distance" */
23+
@SerialName("range_type")
24+
val rangeType: String? = null,
25+
26+
/** e.g. ["area"] (see docs for more) */
27+
val attributes: List<String>? = null,
28+
29+
/** Optional request id that is echoed back in metadata */
30+
val id: String? = null,
31+
32+
/** If true, include intersection info in response */
33+
val intersections: Boolean? = null,
34+
35+
/** If set, returns multiple bands every `interval` (same unit as range_type) */
36+
val interval: Int? = null,
37+
38+
/** "start" (default) or "destination" */
39+
@SerialName("location_type")
40+
val locationType: String? = null,
41+
42+
/** Smoothing factor, e.g. 25 */
43+
val smoothing: Double? = null,
44+
45+
/** Extra tweaks */
46+
val options: IsochronesOptions? = null
47+
)
48+
49+
/**
50+
* Subset of useful options. Extend as needed.
51+
*/
52+
@Serializable
53+
data class IsochronesOptions(
54+
/** "controlled" or "all" */
55+
@SerialName("avoid_borders")
56+
val avoidBorders: String? = null,
57+
58+
/** e.g. ["ferries","tollways"] */
59+
@SerialName("avoid_features")
60+
val avoidFeatures: List<String>? = null,
61+
62+
/**
63+
* Avoid polygons (GeoJSON geometry). Keep generic to stay flexible:
64+
* supply a Feature/Geometry object you serialize yourself.
65+
*/
66+
@SerialName("avoid_polygons")
67+
val avoidPolygons: JsonElement? = null
68+
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.nitri.ors.model.isochrones
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class IsochronesResponse(
8+
val type: String,
9+
val bbox: List<Double>,
10+
val features: List<IsochroneFeature>,
11+
val metadata: IsochronesMetadata
12+
)
13+
14+
@Serializable
15+
data class IsochroneFeature(
16+
val type: String,
17+
val properties: IsochroneProperties,
18+
val geometry: IsochroneGeometry
19+
)
20+
21+
@Serializable
22+
data class IsochroneProperties(
23+
@SerialName("group_index") val groupIndex: Int,
24+
val value: Double, // <-- was Int
25+
val center: List<Double>
26+
)
27+
28+
@Serializable
29+
data class IsochroneGeometry(
30+
val type: String, // "Polygon" (sometimes "MultiPolygon")
31+
// ORS is returning Polygon for your case:
32+
val coordinates: List<List<List<Double>>>
33+
// If you later see MultiPolygon, change to List<List<List<List<Double>>>> or make it polymorphic.
34+
)
35+
36+
@Serializable
37+
data class IsochronesMetadata(
38+
val attribution: String,
39+
val service: String,
40+
val timestamp: Long,
41+
val query: IsochronesQuery,
42+
val engine: IsochronesEngine
43+
)
44+
45+
@Serializable
46+
data class IsochronesQuery(
47+
val profile: String,
48+
val profileName: String,
49+
val locations: List<List<Double>>,
50+
val range: List<Double>, // <-- was List<Int>
51+
@SerialName("range_type") val rangeType: String? = null
52+
)
53+
54+
@Serializable
55+
data class IsochronesEngine(
56+
val version: String,
57+
@SerialName("build_date") val buildDate: String,
58+
@SerialName("graph_date") val graphDate: String,
59+
@SerialName("osm_date") val osmDate: String
60+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.nitri.ors.repository
2+
3+
import org.nitri.ors.api.OpenRouteServiceApi
4+
import org.nitri.ors.model.export.ExportRequest
5+
import org.nitri.ors.model.export.ExportResponse
6+
import org.nitri.ors.model.export.TopoJsonExportResponse
7+
import org.nitri.ors.model.isochrones.IsochronesRequest
8+
import org.nitri.ors.model.isochrones.IsochronesResponse
9+
class IsochronesRepository(private val api: OpenRouteServiceApi) {
10+
11+
suspend fun getIsochrones(
12+
locations: List<List<Double>>,
13+
range: List<Int>,
14+
profile: String,
15+
attributes: List<String>? = null,
16+
rangeType: String? = null,
17+
): IsochronesResponse {
18+
val request = IsochronesRequest(
19+
locations, range, rangeType, attributes
20+
)
21+
return api.getIsochrones(profile, request)
22+
}
23+
24+
}

0 commit comments

Comments
 (0)