mirror of
https://github.com/Pygmalion69/OpenTopoMapViewer.git
synced 2025-10-06 00:02:42 +02:00
Merge branch “feature-ors”
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package org.nitri.ors
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.nitri.ors.client.OpenRouteServiceClient
|
||||
import org.nitri.ors.model.optimization.Job
|
||||
import org.nitri.ors.model.optimization.Vehicle
|
||||
import org.nitri.ors.repository.OptimizationRepository
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class OptimizationInstrumentedTest {
|
||||
|
||||
private fun createRepository(context: Context): OptimizationRepository {
|
||||
val apiKey = context.getString(R.string.ors_api_key)
|
||||
val api = OpenRouteServiceClient.create(apiKey, context)
|
||||
return OptimizationRepository(api)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOptimization_successful() = runBlocking {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val repository = createRepository(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 = repository.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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
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.client.OpenRouteServiceClient
|
||||
import org.nitri.ors.repository.PoisRepository
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PoisInstrumentedTest {
|
||||
|
||||
private fun createRepository(context: Context): PoisRepository {
|
||||
val apiKey = context.getString(R.string.ors_api_key)
|
||||
val api = OpenRouteServiceClient.create(apiKey, context)
|
||||
return PoisRepository(api)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPois_byBbox_successful() = runBlocking {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val repository = createRepository(context)
|
||||
|
||||
// Bounding box around Heidelberg, Germany
|
||||
val bbox = listOf(
|
||||
listOf(8.67, 49.40), // minLon, minLat
|
||||
listOf(8.70, 49.43) // maxLon, maxLat
|
||||
)
|
||||
|
||||
val response = repository.getPoisByBbox(
|
||||
bbox = bbox,
|
||||
limit = 10
|
||||
)
|
||||
|
||||
assertNotNull("POIs response should not be null", response)
|
||||
assertEquals("GeoJSON type should be FeatureCollection", "FeatureCollection", response.type)
|
||||
assertNotNull("Information block should be present", response.information)
|
||||
assertTrue("Features should not be empty in a city bbox", response.features.isNotEmpty())
|
||||
|
||||
val first = response.features.first()
|
||||
assertEquals("Feature type should be Feature", "Feature", first.type)
|
||||
assertEquals("Geometry type should be Point", "Point", first.geometry.type)
|
||||
assertEquals("Point coordinates should be [lon, lat]", 2, first.geometry.coordinates.size)
|
||||
|
||||
// Basic properties sanity
|
||||
val props = first.properties
|
||||
assertTrue("OSM id should be positive", props.osmId > 0)
|
||||
assertTrue("Distance should be non-negative", props.distance >= 0.0)
|
||||
}
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
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.client.OpenRouteServiceClient
|
||||
import org.nitri.ors.repository.SnapRepository
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SnapInstrumentedTest {
|
||||
|
||||
private fun createRepository(context: Context): SnapRepository {
|
||||
val apiKey = context.getString(R.string.ors_api_key)
|
||||
val api = OpenRouteServiceClient.create(apiKey, context)
|
||||
return SnapRepository(api)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSnap_successful() = runBlocking {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val repository = createRepository(context)
|
||||
|
||||
// Two locations in/near Heidelberg, Germany [lon, lat]
|
||||
val locations = listOf(
|
||||
listOf(8.681495, 49.41461), // Heidelberg center
|
||||
listOf(8.687872, 49.420318) // Nearby point
|
||||
)
|
||||
val profile = "driving-car"
|
||||
val radius = 50 // meters
|
||||
|
||||
val response = repository.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 repository = createRepository(context)
|
||||
|
||||
val locations = listOf(
|
||||
listOf(8.681495, 49.41461),
|
||||
listOf(8.687872, 49.420318)
|
||||
)
|
||||
val profile = "driving-car"
|
||||
val radius = 50
|
||||
|
||||
val response = repository.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 repository = createRepository(context)
|
||||
|
||||
val locations = listOf(
|
||||
listOf(8.681495, 49.41461),
|
||||
listOf(8.687872, 49.420318)
|
||||
)
|
||||
val profile = "driving-car"
|
||||
val radius = 50
|
||||
|
||||
val response = repository.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)
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
package org.nitri.ors.api
|
||||
|
||||
import org.nitri.ors.model.matrix.MatrixResponse
|
||||
import okhttp3.ResponseBody
|
||||
import org.nitri.ors.model.export.ExportRequest
|
||||
import org.nitri.ors.model.export.ExportResponse
|
||||
@@ -8,9 +7,17 @@ import org.nitri.ors.model.export.TopoJsonExportResponse
|
||||
import org.nitri.ors.model.isochrones.IsochronesRequest
|
||||
import org.nitri.ors.model.isochrones.IsochronesResponse
|
||||
import org.nitri.ors.model.matrix.MatrixRequest
|
||||
import org.nitri.ors.model.matrix.MatrixResponse
|
||||
import org.nitri.ors.model.optimization.OptimizationRequest
|
||||
import org.nitri.ors.model.optimization.OptimizationResponse
|
||||
import org.nitri.ors.model.pois.PoisGeoJsonResponse
|
||||
import org.nitri.ors.model.pois.PoisRequest
|
||||
import org.nitri.ors.model.route.GeoJsonRouteResponse
|
||||
import org.nitri.ors.model.route.RouteRequest
|
||||
import org.nitri.ors.model.route.RouteResponse
|
||||
import org.nitri.ors.model.snap.SnapGeoJsonResponse
|
||||
import org.nitri.ors.model.snap.SnapRequest
|
||||
import org.nitri.ors.model.snap.SnapResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
@@ -20,6 +27,8 @@ import retrofit2.http.Query
|
||||
|
||||
interface OpenRouteServiceApi {
|
||||
|
||||
// Directions
|
||||
|
||||
@GET("v2/directions/{profile}")
|
||||
suspend fun getRouteSimple(
|
||||
@Path("profile") profile: String,
|
||||
@@ -84,5 +93,38 @@ interface OpenRouteServiceApi {
|
||||
@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
|
||||
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import org.nitri.ors.api.OpenRouteServiceApi
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object OpenRouteServiceClient {
|
||||
fun create(apiKey: String, context: Context): OpenRouteServiceApi {
|
||||
@@ -27,18 +28,26 @@ object OpenRouteServiceClient {
|
||||
val original = chain.request()
|
||||
|
||||
val newRequest = original.newBuilder()
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
// 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)
|
||||
}
|
||||
// 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()
|
||||
|
@@ -0,0 +1,162 @@
|
||||
package org.nitri.ors.model.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
|
||||
)
|
@@ -0,0 +1,112 @@
|
||||
package org.nitri.ors.model.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
|
||||
)
|
@@ -0,0 +1,26 @@
|
||||
package org.nitri.ors.model.pois
|
||||
|
||||
import kotlinx.serialization.Required
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PoisRequest(
|
||||
@Required val request: String = "pois",
|
||||
val geometry: Geometry,
|
||||
val filters: Map<String, String>? = null,
|
||||
val limit: Int? = null,
|
||||
val sortby: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Geometry(
|
||||
val bbox: List<List<Double>>? = null, // [[minLon,minLat],[maxLon,maxLat]]
|
||||
val geojson: GeoJsonGeometry? = null, // optional: GeoJSON geometry
|
||||
val buffer: Int? = null // optional: buffer in meters
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GeoJsonGeometry(
|
||||
val type: String, // e.g., "Point"
|
||||
val coordinates: List<Double> // [lon, lat]
|
||||
)
|
@@ -0,0 +1,83 @@
|
||||
package org.nitri.ors.model.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 request’s geometry wrapper */
|
||||
@Serializable
|
||||
data class PoisQueryGeometry(
|
||||
val bbox: List<List<Double>>? = null, // [[minLon,minLat],[maxLon,maxLat]]
|
||||
val geojson: GeoJsonGeometry? = null,
|
||||
val buffer: Int? = null
|
||||
)
|
||||
|
@@ -0,0 +1,43 @@
|
||||
package org.nitri.ors.model.snap
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.nitri.ors.model.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]
|
||||
)
|
@@ -0,0 +1,17 @@
|
||||
package org.nitri.ors.model.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
|
||||
)
|
@@ -0,0 +1,30 @@
|
||||
package org.nitri.ors.model.snap
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.nitri.ors.model.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
|
||||
)
|
@@ -0,0 +1,46 @@
|
||||
package org.nitri.ors.repository
|
||||
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import org.nitri.ors.api.OpenRouteServiceApi
|
||||
import org.nitri.ors.model.optimization.CustomMatrix
|
||||
import org.nitri.ors.model.optimization.Job
|
||||
import org.nitri.ors.model.optimization.OptimizationRequest
|
||||
import org.nitri.ors.model.optimization.OptimizationResponse
|
||||
import org.nitri.ors.model.optimization.Shipment
|
||||
import org.nitri.ors.model.optimization.Vehicle
|
||||
|
||||
/**
|
||||
* Repository for the OpenRouteService Optimization endpoint.
|
||||
*
|
||||
* This is a thin wrapper around the Retrofit API, similar to other repositories
|
||||
* in this package. The repository builds the OptimizationRequest from the
|
||||
* provided arguments.
|
||||
*/
|
||||
class OptimizationRepository(private val api: OpenRouteServiceApi) {
|
||||
|
||||
/**
|
||||
* Calls the ORS Optimization endpoint with provided arguments and builds the request.
|
||||
*
|
||||
* @param vehicles Required list of vehicles.
|
||||
* @param jobs Optional list of jobs.
|
||||
* @param shipments Optional list of shipments.
|
||||
* @param matrices Optional custom matrices keyed by profile.
|
||||
* @param options Optional free-form options.
|
||||
*/
|
||||
suspend fun getOptimization(
|
||||
vehicles: List<Vehicle>,
|
||||
jobs: List<Job>? = null,
|
||||
shipments: List<Shipment>? = null,
|
||||
matrices: Map<String, CustomMatrix>? = null,
|
||||
options: Map<String, JsonElement>? = null
|
||||
): OptimizationResponse {
|
||||
val request = OptimizationRequest(
|
||||
jobs = jobs,
|
||||
shipments = shipments,
|
||||
vehicles = vehicles,
|
||||
matrices = matrices,
|
||||
options = options
|
||||
)
|
||||
return api.getOptimization(request)
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package org.nitri.ors.repository
|
||||
|
||||
import org.nitri.ors.api.OpenRouteServiceApi
|
||||
import org.nitri.ors.model.pois.GeoJsonGeometry
|
||||
import org.nitri.ors.model.pois.Geometry
|
||||
import org.nitri.ors.model.pois.PoisGeoJsonResponse
|
||||
import org.nitri.ors.model.pois.PoisRequest
|
||||
|
||||
class PoisRepository(private val api: OpenRouteServiceApi) {
|
||||
|
||||
/**
|
||||
* 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 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 api.getPois(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query POIs around a point with a buffer radius.
|
||||
*
|
||||
* @param point [lon, lat]
|
||||
* @param buffer Buffer radius in meters
|
||||
*/
|
||||
suspend fun 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 api.getPois(request)
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
package org.nitri.ors.repository
|
||||
|
||||
import org.nitri.ors.api.OpenRouteServiceApi
|
||||
import org.nitri.ors.model.snap.SnapGeoJsonResponse
|
||||
import org.nitri.ors.model.snap.SnapRequest
|
||||
import org.nitri.ors.model.snap.SnapResponse
|
||||
|
||||
class SnapRepository(private val api: OpenRouteServiceApi) {
|
||||
|
||||
/**
|
||||
* Calls the ORS Snap endpoint for the given profile.
|
||||
*
|
||||
* @param locations List of [lon, lat] coordinates to snap.
|
||||
* @param radius Maximum radius (meters) around given coordinates to search for graph edges.
|
||||
* @param profile ORS profile, e.g. "driving-car", "foot-hiking", etc.
|
||||
* @param id Optional arbitrary request id echoed back by the service.
|
||||
*/
|
||||
suspend fun getSnap(
|
||||
locations: List<List<Double>>,
|
||||
radius: Int,
|
||||
profile: String,
|
||||
id: String? = null,
|
||||
): SnapResponse {
|
||||
val request = SnapRequest(
|
||||
locations = locations,
|
||||
radius = radius,
|
||||
id = id
|
||||
)
|
||||
return api.getSnap(profile, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the ORS Snap JSON endpoint.
|
||||
*/
|
||||
suspend fun getSnapJson(
|
||||
locations: List<List<Double>>,
|
||||
radius: Int,
|
||||
profile: String,
|
||||
id: String? = null,
|
||||
): SnapResponse {
|
||||
val request = SnapRequest(
|
||||
locations = locations,
|
||||
radius = radius,
|
||||
id = id
|
||||
)
|
||||
return api.getSnapJson(profile, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the ORS Snap GeoJSON endpoint.
|
||||
*/
|
||||
suspend fun getSnapGeoJson(
|
||||
locations: List<List<Double>>,
|
||||
radius: Int,
|
||||
profile: String,
|
||||
id: String? = null,
|
||||
): SnapGeoJsonResponse {
|
||||
val request = SnapRequest(
|
||||
locations = locations,
|
||||
radius = radius,
|
||||
id = id
|
||||
)
|
||||
return api.getSnapGeoJson(profile, request)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user