Get nominatim info for a location and date with maplibre

I’m looking to integrate the open history map in an application but I can’t figure out how to query the self hosted nominatim api for the information on where the user clicks. Can someone give me a hand on this regard?

The language I’m using is kotlin

    MapView(
        modifier = modifier,
        styleUrl = styleUri,
        mapControls = MapControls(
            attribution = AttributionSettings(enabled = false),
            logo = LogoSettings(enabled = false)
        ),
        camera = mapCamera,
        onMapReadyCallback = { style ->
            loaded = true
            mapStyle = style
            style.filterLayersByDate(dateRange)
        },
        onTapGestureCallback = { tapGestureContext ->
        }
    )

In case it helps, here’s the documentation for OHM’s instance of Nominatim. Nominatim itself has documentation on reverse geocoding (converting geographic coordinates to an address). But first you’ll have to convert the gesture’s screen coordinates to geographic coordinates.

Unfortunately, our Nominatim instance is currently not in a very usable state for reverse geocoding:

If you’re primarily interested in knowing the location in terms of administrative boundaries (country, city, etc.), as opposed to street addresses, you might consider querying the Overpass API or QLever instead.

By the way, which library are you using in Kotlin? Is it for an Android application or a cross-platform application? We publish a support library for filtering the map by date, but it’s only available for MapLibre/Mapbox GL JS so far. Is this filterLayersByDate() method something you’ve written for Kotlin?

Thanks will look into those.

About kotlin, I’m using maplibre-compose-playground for now until an issue in ramani-maps is fixed.

and the filterLayersByDate method it’s a Style extension I’ve written based on the GL JS example.

import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.layers.CircleLayer
import org.maplibre.android.style.layers.FillExtrusionLayer
import org.maplibre.android.style.layers.FillLayer
import org.maplibre.android.style.layers.HeatmapLayer
import org.maplibre.android.style.layers.Layer
import org.maplibre.android.style.layers.LineLayer
import org.maplibre.android.style.layers.SymbolLayer

import java.time.LocalDate
import java.time.temporal.ChronoUnit
import kotlin.math.floor

data class DateRange(
    val startDecimalYear: Double,
    val endDecimalYear: Double
) {
    val startISODate: String
        get() = decimalYearToISODate(startDecimalYear)

    val endISODate: String
        get() = decimalYearToISODate(endDecimalYear)

    companion object {
        fun fromDate(date: LocalDate): DateRange {
            val year = date.year
            val startOfYear = LocalDate.of(year, 1, 1)
            val startOfNextYear = LocalDate.of(year + 1, 1, 1)

            val daysInYear = ChronoUnit.DAYS.between(startOfYear, startOfNextYear)
            val dayOfYear = ChronoUnit.DAYS.between(startOfYear, date)

            val decimalYear = year + (dayOfYear.toDouble() / daysInYear)
            return DateRange(decimalYear, decimalYear)
        }

        private fun decimalYearToISODate(decimalYear: Double): String {
            return decimalYearToLocalDate(decimalYear).toString()
        }

        fun decimalYearToLocalDate(decimalYear: Double): LocalDate {
            val year = floor(decimalYear).toInt()
            val fractionOfYear = decimalYear - year

            val startOfYear = LocalDate.of(year, 1, 1)
            val startOfNextYear = LocalDate.of(year + 1, 1, 1)
            val daysInYear = ChronoUnit.DAYS.between(startOfYear, startOfNextYear)

            val dayOfYear = (fractionOfYear * daysInYear).toLong()

            return startOfYear.plusDays(dayOfYear)
        }
    }
}

fun Style.filterLayersByDate(date: LocalDate) {
    val dateRange = DateRange.fromDate(date)
    for (layer in this.layers) {
        when (layer) {
            is LineLayer -> {
                if (!originalFilters.containsKey(layer.id)) {
                    originalFilters[layer.id] = layer.filter
                }
                layer.resetFilter()
                layer.setFilter(constrainExpressionFilterByDateRange(originalFilters[layer.id], dateRange))
            }

            is FillLayer -> {
                if (!originalFilters.containsKey(layer.id)) {
                    originalFilters[layer.id] = layer.filter
                }
                layer.resetFilter()
                layer.setFilter(constrainExpressionFilterByDateRange(originalFilters[layer.id], dateRange))
            }

            is CircleLayer -> {
                if (!originalFilters.containsKey(layer.id)) {
                    originalFilters[layer.id] = layer.filter
                }
                layer.resetFilter()
                layer.setFilter(constrainExpressionFilterByDateRange(originalFilters[layer.id], dateRange))
            }

            is SymbolLayer -> {
                if (!originalFilters.containsKey(layer.id)) {
                    originalFilters[layer.id] = layer.filter
                }
                layer.resetFilter()
                layer.setFilter(constrainExpressionFilterByDateRange(originalFilters[layer.id], dateRange))
            }

            is HeatmapLayer -> {
                if (!originalFilters.containsKey(layer.id)) {
                    originalFilters[layer.id] = layer.filter
                }
                layer.resetFilter()
                layer.setFilter(constrainExpressionFilterByDateRange(originalFilters[layer.id], dateRange))
            }

            is FillExtrusionLayer -> {
                if (!originalFilters.containsKey(layer.id)) {
                    originalFilters[layer.id] = layer.filter
                }
                layer.resetFilter()
                layer.setFilter(constrainExpressionFilterByDateRange(originalFilters[layer.id], dateRange))
            }

            else -> null
        }
    }
}


private fun constrainExpressionFilterByDateRange(
    filter: Expression? = null,
    dateRange: DateRange,
    variablePrefix: String = "maplibre_gl_dates"
): Expression {
    val startDecimalYearVariable = "${variablePrefix}__startDecimalYear"
    val startISODateVariable = "${variablePrefix}__startISODate"
    val endDecimalYearVariable = "${variablePrefix}__endDecimalYear"
    val endISODateVariable = "${variablePrefix}__endISODate"

    val dateConstraints = Expression.all(
        Expression.any(
            Expression.all(
                Expression.has("start_decdate"),
                Expression.lt(
                    Expression.get("start_decdate"),
                    Expression.`var`(endDecimalYearVariable)
                )
            ),
            Expression.all(
                Expression.not(Expression.has("start_decdate")),
                Expression.has("start_date"),
                Expression.lt(
                    Expression.get("start_date"),
                    Expression.`var`(startISODateVariable)
                )
            ),
            Expression.all(
                Expression.not(Expression.has("start_decdate")),
                Expression.not(Expression.has("start_date"))
            )
        ),
        Expression.any(
            Expression.all(
                Expression.has("end_decdate"),
                Expression.gte(
                    Expression.get("end_decdate"),
                    Expression.`var`(startDecimalYearVariable)
                )
            ),
            Expression.all(
                Expression.not(Expression.has("end_decdate")),
                Expression.has("end_date"),
                Expression.gte(
                    Expression.get("end_date"),
                    Expression.`var`(startISODateVariable)
                )
            ),
            Expression.all(
                Expression.not(Expression.has("end_decdate")),
                Expression.not(Expression.has("end_date"))
            )
        )
    )

    val finalExpression = if (filter != null) {
        Expression.all(dateConstraints, filter)
    } else {
        dateConstraints
    }

    return Expression.let(
        Expression.literal(startDecimalYearVariable), Expression.literal(dateRange.startDecimalYear),
        Expression.let(
            Expression.literal(startISODateVariable), Expression.literal(dateRange.startISODate),
            Expression.let(
                Expression.literal(endDecimalYearVariable), Expression.literal(dateRange.endDecimalYear),
                Expression.let(
                    Expression.literal(endISODateVariable), Expression.literal(dateRange.endISODate),
                    finalExpression
                )
            )
        )
    )
}

fun Layer.resetFilter() {
    originalFilters[this.id]?.let { originalFilter ->
        when (this) {
            is LineLayer -> setFilter(originalFilter)
            is FillLayer -> setFilter(originalFilter)
            is CircleLayer -> setFilter(originalFilter)
            is SymbolLayer -> setFilter(originalFilter)
            is HeatmapLayer -> setFilter(originalFilter)
            is FillExtrusionLayer -> setFilter(originalFilter)
            else -> {}
        }
    }
}

I’ve uploaded my kotlin implementation of the date filtering to github gists under the MIT license

1 Like

Thank you so much! I’ve added a link to this gist to the OHM reuse documentation and the maplibre-gl-dates readme.

There are some Android libraries for working with the Overpass API, but I don’t know if they can handle the type of query needed for getting the containing administrative boundaries. Otherwise, a more manual GET or POST request from your preferred HTTP library should do the trick. If you need more than just the identity of the containing boundaries, such as the geometries, there might be libraries for converting Overpass JSON or OSM XML to GeoJSON, like there is for JavaScript.

2 Likes

Nice, thanks for including it in your documentations.

Yeah I did found a kotlin implementation that was mentioned there which is pretty new. But what brought my attention is overpasser since looks more robust.

1 Like

None of those libraries really worked so I’ve created the requests with okhttp. I do have a question about the overpass query.

Is the following correct? Because for example for the year 1994 I don’t get any results when I press for example for “Regatul Romaniei”. Shouldn’t it show some info?

[out:json];
is_in(45.41945810308698,24.974487379859255)->.a;
rel(pivot.a)[boundary=administrative][admin_level=2](if: t["start_date"] <= "1944")[!"end_date"];
out;

The Overpass API example is a bit too specific. If you’re expecting to get Relation: ‪Kingdom of Romania (1919-1921)‬ (‪2693464‬) | OpenHistoricalMap for the year 1944, then the query shouldn’t have [!"end_date"], because that boundary has an end_date of 1921. Instead, use an evaluator like (if: t["start_date"] < "1945" && t["end_date"] >= "1944"). Note that the first clause is a year ahead, because 1945-12-31 sorts after 1945.

1 Like

Oh, I played a little with the queries and I got the idea of using comparators for end date but I couldn’t get it working due to the first clause needing to be a year ahead.

Now it works, I also had to check if there’s no end_date to also show more recent relations.

Thanks for the help.

[out:json];
is_in(45.279679137105006,24.311249230688873)->.a;
rel(pivot.a)[boundary=administrative][admin_level=2](if: t["start_date"] < "1945" && (!is_tag("end_date") || t["end_date"] >= "1944"));
out;

1 Like