Hi,
I’m working on a Kotlin application using GraphHopper Core 10.2. My goal is to dynamically adjust the route when the user provides obstacles (as Point, Line, or Polygon). The expected behavior: if the route intersects any user-defined obstacle, GraphHopper should calculate an alternative path avoiding it.
I implemented this by dynamically generating JsonFeature
objects, adding them to a JsonFeatureCollection
, then setting it as model.areas
in a CustomModel
.
For each obstacle, I also add a speed and priority constraint using area conditions (like in_barrier_0
My code :
import com.google.gson.*
import com.graphhopper.json.Statement
import com.graphhopper.util.CustomModel
import com.graphhopper.util.JsonFeature
import com.graphhopper.util.JsonFeatureCollection
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.io.geojson.GeoJsonReader
import org.locationtech.jts.io.geojson.GeoJsonWriter
import org.locationtech.jts.geom.Envelope
import model.Barrier
import model.BarrierType
import java.lang.reflect.Type
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
class AppState {
private val gson = GsonBuilder()
.registerTypeAdapter(Geometry::class.java, GeometrySerializer())
.registerTypeAdapter(Geometry::class.java, GeometryDeserializer())
.setPrettyPrinting()
.create()
fun buildCustomModel(barriers: List<Barrier>): CustomModel {
val model = CustomModel()
.addToSpeed(Statement.If("!car_access || car_access == false", Statement.Op.LIMIT, "0.0"))
.addToSpeed(Statement.ElseIf("road_class == MOTORWAY", Statement.Op.LIMIT, "130"))
.addToSpeed(Statement.ElseIf("road_class == TRUNK", Statement.Op.LIMIT, "110"))
.addToSpeed(Statement.ElseIf("road_class == PRIMARY", Statement.Op.LIMIT, "80"))
.addToSpeed(Statement.Else(Statement.Op.LIMIT, "50"))
val featuresList = mutableListOf<JsonFeature>()
barriers.forEachIndexed { i, barrier ->
val areaId = "barrier_$i"
val jsonFeature: JsonFeature? = when (barrier.type) {
BarrierType.POINT -> {
val point = barrier.coordinates.first()
val size = (barrier.properties["size"] as? Number)?.toDouble() ?: 10.0
createCircularArea(point.lat, point.lng, size)
}
BarrierType.LINE -> {
createLineArea(
barrier.coordinates.map { it.lat to it.lng },
10.0
)
}
BarrierType.POLYGON -> {
createPolygonArea(
barrier.coordinates.map { it.lat to it.lng }
)
}
else -> null
}
if (jsonFeature != null) {
jsonFeature.properties["id"] = areaId
featuresList.add(jsonFeature)
model.addToSpeed(Statement.If("in_$areaId", Statement.Op.LIMIT, "0.0"))
model.addToPriority(Statement.If("in_$areaId", Statement.Op.MULTIPLY, "0.0"))
}
}
if (featuresList.isNotEmpty()) {
val barrierCollection = JsonFeatureCollection()
barrierCollection.features.addAll(featuresList)
model.areas = barrierCollection
}
return model
.setDistanceInfluence(50.0)
.setHeadingPenalty(30.0)
}
private fun createCircularArea(lat: Double, lon: Double, radius: Double): JsonFeature {
val radiusDeg = radius / 111320.0
val points = (0..35).map { i ->
val angle = Math.toRadians(i * 10.0)
listOf(lon + radiusDeg * cos(angle), lat + radiusDeg * sin(angle))
}
val geoJsonFeatureMap = mutableMapOf(
"type" to "Feature",
"geometry" to mapOf(
"type" to "Polygon",
"coordinates" to listOf(points + points.first())
),
"properties" to mutableMapOf<String, Any>()
)
return convertMapToJsonFeature(geoJsonFeatureMap)
}
private fun createLineArea(points: List<Pair<Double, Double>>, width: Double): JsonFeature {
val widthDeg = width / 111320.0
val polygons = mutableListOf<List<List<Double>>>()
points.windowed(2, 1) { (start, end) ->
val latDiff = end.first - start.first
val lonDiff = end.second - start.second
val length = sqrt(latDiff * latDiff + lonDiff * lonDiff)
if (length == 0.0) return@windowed
val normalLon = -latDiff / length * widthDeg
val normalLat = lonDiff / length * widthDeg
val polyPoints = listOf(
listOf(start.second + normalLon, start.first + normalLat),
listOf(end.second + normalLon, end.first + normalLat),
listOf(end.second - normalLon, end.first - normalLat),
listOf(start.second - normalLon, start.first - normalLat),
listOf(start.second + normalLon, start.first + normalLat)
)
polygons.add(listOf(polyPoints))
}
val geoJsonFeatureMap = mutableMapOf(
"type" to "Feature",
"geometry" to mapOf(
"type" to "MultiPolygon",
"coordinates" to polygons
),
"properties" to mutableMapOf<String, Any>()
)
return convertMapToJsonFeature(geoJsonFeatureMap)
}
private fun createPolygonArea(points: List<Pair<Double, Double>>): JsonFeature {
val closedPoints = if (points.first() == points.last()) points else points + points.first()
val geoJsonFeatureMap = mutableMapOf(
"type" to "Feature",
"geometry" to mapOf(
"type" to "Polygon",
"coordinates" to listOf(closedPoints.map { listOf(it.second, it.first) })
),
"properties" to mutableMapOf<String, Any>()
)
return convertMapToJsonFeature(geoJsonFeatureMap)
}
private fun convertMapToJsonFeature(map: Map<String, Any>): JsonFeature {
val jsonString = gson.toJson(map)
val geoJsonReader = GeoJsonReader()
val geometry = geoJsonReader.read(jsonString)
@Suppress("UNCHECKED_CAST")
val properties = map["properties"] as? MutableMap<String, Any> ?: mutableMapOf()
val envelope = geometry.envelopeInternal
return JsonFeature(
null,
"Feature",
envelope,
geometry,
properties
)
}
class GeometrySerializer : JsonSerializer<Geometry> {
override fun serialize(src: Geometry?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
if (src == null) return JsonNull.INSTANCE
val writer = GeoJsonWriter()
return JsonParser.parseString(writer.write(src))
}
}
class GeometryDeserializer : JsonDeserializer<Geometry> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Geometry? {
if (json == null || json.isJsonNull) return null
val reader = GeoJsonReader()
return reader.read(json.toString())
}
}
}