Add rake overpass:pois to fetch camper POIs from Overpass
lib/maptool/overpass_pois.rb: queries Overpass for drinking water, dump stations, LPG refill, toilets, laundromats, supermarkets/ convenience stores, and springs within a bbox, writes GeoJSON. A dev-time tool (like the offline.pmtiles/terrain.pmtiles generation steps already documented) — not called by the running app, drop the output into data/gpx/ or upload it through the Tracks panel like any other .geojson layer. Verified against the real Overpass API (88 POIs for central Bratislava) in addition to the new unit tests (pure to_geojson/ build_query logic, no network in the suite itself). Full suite: 103 runs, 0 failures.
This commit is contained in:
1 parent
aa2b96ac70
commit
f53c74e06f
5 files changed
+180
-1
No files matched your search
@@ -0,0 +1,81 @@
|
||||
require "net/http"
|
||||
require "json"
|
||||
|
||||
module Maptool
|
||||
# A dev-time tool (see the "overpass:pois" Rake task), not something the
|
||||
# deployed app calls itself — same "generate locally, drop the result in
|
||||
# data/gpx/" story as offline.pmtiles/terrain.pmtiles (docs/development.adoc),
|
||||
# kept off the running server so it never depends on Overpass's public
|
||||
# instance being up, or eats into its (fairly strict, shared) rate limits
|
||||
# on every page load.
|
||||
module OverpassPois
|
||||
class FetchError < StandardError; end
|
||||
|
||||
OVERPASS_API = "https://overpass-api.de/api/interpreter".freeze
|
||||
|
||||
# Each entry: an Overpass tag filter -> a human category label, used
|
||||
# both to build the query (unioning every filter) and to label results
|
||||
# back (matched by scanning an element's own tags for the same
|
||||
# key/value, not by which branch of the query found it — Overpass's
|
||||
# `out body` doesn't say which union member matched).
|
||||
CATEGORIES = {
|
||||
{ "amenity" => "drinking_water" } => "Drinking water",
|
||||
{ "amenity" => "sanitary_dump_station" } => "Dump station",
|
||||
{ "amenity" => "fuel", "fuel:lpg" => "yes" } => "LPG refill",
|
||||
{ "shop" => "gas" } => "LPG refill",
|
||||
{ "amenity" => "toilets" } => "Public toilet",
|
||||
{ "shop" => "laundry" } => "Laundry",
|
||||
{ "shop" => "supermarket" } => "Supermarket",
|
||||
{ "shop" => "convenience" } => "Convenience store",
|
||||
{ "natural" => "spring" } => "Spring",
|
||||
}.freeze
|
||||
|
||||
# bbox: [south, west, north, east] (Overpass's own axis order).
|
||||
def self.fetch(bbox, timeout: 60)
|
||||
query = build_query(bbox, timeout: timeout)
|
||||
uri = URI(OVERPASS_API)
|
||||
response = Net::HTTP.post_form(uri, "data" => query)
|
||||
raise FetchError, "unexpected response #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
||||
|
||||
JSON.parse(response.body)
|
||||
end
|
||||
|
||||
def self.build_query(bbox, timeout: 60)
|
||||
bbox_str = bbox.join(",")
|
||||
filters = CATEGORIES.keys.map do |tags|
|
||||
conditions = tags.map { |k, v| %(["#{k}"="#{v}"]) }.join
|
||||
"node#{conditions}(#{bbox_str});"
|
||||
end.join("\n ")
|
||||
"[out:json][timeout:#{timeout}];\n(\n #{filters}\n);\nout body;"
|
||||
end
|
||||
|
||||
# Pure — no network — so it's cheaply testable against a canned
|
||||
# Overpass response. Only `type: "node"` elements are handled: ways/
|
||||
# relations (a mapped-as-an-area supermarket, say) would need their own
|
||||
# centroid computation, and the categories above are overwhelmingly
|
||||
# mapped as plain nodes in practice.
|
||||
def self.to_geojson(overpass_response)
|
||||
features = (overpass_response["elements"] || []).filter_map do |element|
|
||||
next unless element["type"] == "node"
|
||||
|
||||
category = category_for(element["tags"] || {})
|
||||
next unless category
|
||||
|
||||
{
|
||||
"type" => "Feature",
|
||||
"properties" => { "name" => (element["tags"] || {})["name"] || category, "category" => category },
|
||||
"geometry" => { "type" => "Point", "coordinates" => [element["lon"], element["lat"]] },
|
||||
}
|
||||
end
|
||||
{ "type" => "FeatureCollection", "features" => features }
|
||||
end
|
||||
|
||||
def self.category_for(tags)
|
||||
CATEGORIES.each do |filter, label|
|
||||
return label if filter.all? { |k, v| tags[k] == v }
|
||||
end
|
||||
nil
|
||||
end
|
||||
private_class_method :category_for
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user