Dynamic Obstacles in CustomModel - Route Calculation Fails with JsonFeature (GraphHopper 10.2 Kotlin)

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())
    }
}

}