Replace the per-popup "Nearby on Wikipedia" link with a live Wikipedia layer
The old link only ever surfaced one nearby article at a time, buried in whatever POI's popup you happened to open, and only for uploaded files with a category. Replaced it with a standalone "Wikipedia" toggle under the renamed "Tracks & Points" panel: enabling it adds a live map layer that refetches GET /api/kiwix/nearby on every pan/zoom and plots every nearby article as its own point. kiwix-serve's own geo search never echoes back a result's coordinates (confirmed live against the real production ZIM, both its HTML and ?format=xml output) — it only filters by distance from a point. So Maptool::Kiwix.nearby fetches each matching article's own page too and reads its <meta name="geo.position"> tag, which every geotagged Wikipedia article carries. Tried parallelizing those per-article fetches with threads first; measured no improvement against the real ZIM (kiwix-serve appears to serialize requests regardless), so it stayed sequential — ~1.6s for a 30-result page, fine for a refetch-on-pan-end layer. Clicking a point opens a small popup linking to the article (proxied through the existing /kiwix/* passthrough). New WIKI_ICON (NPS's bookstore-22.svg) marks these, kept out of POI_ICONS since that set doubles as notes.js's icon-picker option list and this fixed-glyph layer has no per-feature category to key off of.
This commit is contained in:
1 parent
47e3f9c7e8
commit
787df5478d
9 files changed
+354
-14
No files matched your search
@@ -1,4 +1,5 @@
|
||||
require "net/http"
|
||||
require "rexml/document"
|
||||
|
||||
module Maptool
|
||||
# A reachability check plus a passthrough proxy for an external
|
||||
@@ -10,6 +11,13 @@ module Maptool
|
||||
# *this* app, leaving no address a browser could reach kiwix-serve at
|
||||
# directly. GET /kiwix/* in app.rb calls .proxy below instead.
|
||||
module Kiwix
|
||||
# <meta name="geo.position" content="LAT;LON"> — a standard tag
|
||||
# Wikipedia's own MediaWiki geo extension puts on every geotagged
|
||||
# article's page (confirmed live against a real ZIM), semicolon rather
|
||||
# than comma-separated. Far simpler to match than the human-facing
|
||||
# "48°08′46″N 17°10′38″E / 48.14611°N 17.17722°E" markup the article
|
||||
# body itself renders (DMS *and* decimal, in varying wrapper markup).
|
||||
GEO_POSITION_PATTERN = /<meta name="geo\.position" content="(-?[\d.]+);(-?[\d.]+)"/
|
||||
def self.reachable?(url, open_timeout: 1, read_timeout: 2)
|
||||
return false if url.to_s.strip.empty?
|
||||
|
||||
@@ -41,5 +49,82 @@ module Maptool
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
||||
# bbox: [south, west, north, east] — the "Wikipedia" map layer
|
||||
# (public/js/gpx.js) refetches this on every pan/zoom for whatever's
|
||||
# currently on screen. kiwix-serve's own geo search only *filters* by
|
||||
# distance from a point, it doesn't echo each result's own coordinates
|
||||
# back (confirmed live: neither its HTML nor its ?format=xml output
|
||||
# carries one) — so every matching article's page is fetched too, and
|
||||
# its coordinates read from GEO_POSITION_PATTERN above. Cheap since
|
||||
# kiwix-serve serves its ZIM locally with no network round trip, unlike
|
||||
# this same fetch-every-result shape would be against live Wikipedia.
|
||||
def self.nearby(base_url, bbox, limit: 30, open_timeout: 2, read_timeout: 10)
|
||||
south, west, north, east = bbox
|
||||
lat = (south + north) / 2.0
|
||||
lng = (west + east) / 2.0
|
||||
radius_m = (haversine_km(lat, lng, north, east) * 1000).round
|
||||
|
||||
query = URI.encode_www_form(pattern: "", latitude: lat, longitude: lng, distance: radius_m, pageLength: limit, format: "xml")
|
||||
response = proxy(base_url, "/kiwix/search", query, open_timeout: open_timeout, read_timeout: read_timeout)
|
||||
return empty_collection unless response&.code == "200"
|
||||
|
||||
# One request per result — tried running these in parallel threads
|
||||
# first (each is blocking I/O, so MRI's GIL releases during the
|
||||
# actual wait), but measured no improvement against the real
|
||||
# production ZIM (kiwix-serve itself appears to serialize incoming
|
||||
# requests regardless), so plain sequential stayed. ~1.6s for a
|
||||
# 30-result page on that box — fine for a refetch-on-pan-end layer,
|
||||
# not for anything tighter; lower `limit` first if that's ever not
|
||||
# enough headroom, before reaching for something more involved.
|
||||
features = parse_search_results(response.body).filter_map { |result| point_feature(base_url, result, open_timeout, read_timeout) }
|
||||
{ "type" => "FeatureCollection", "features" => features }
|
||||
end
|
||||
|
||||
def self.empty_collection
|
||||
{ "type" => "FeatureCollection", "features" => [] }
|
||||
end
|
||||
private_class_method :empty_collection
|
||||
|
||||
def self.parse_search_results(xml)
|
||||
doc = REXML::Document.new(xml)
|
||||
(doc.root&.get_elements("channel/item") || []).filter_map do |item|
|
||||
title = item.elements["title"]&.text
|
||||
link = item.elements["link"]&.text
|
||||
{ title: title, link: link } if title && link
|
||||
end
|
||||
rescue REXML::ParseException
|
||||
[]
|
||||
end
|
||||
private_class_method :parse_search_results
|
||||
|
||||
# result[:link] arrives already percent-encoded, straight out of
|
||||
# kiwix-serve's own XML (e.g. "/kiwix/content/wikipedia/%C5%A0tadi%C3%B3n_Rapid")
|
||||
# — decoded first so .proxy's own escaping (built for Sinatra's
|
||||
# already-*decoded* splat, see its own comment above) doesn't double-
|
||||
# encode the "%" and 404 the request.
|
||||
def self.point_feature(base_url, result, open_timeout, read_timeout)
|
||||
response = proxy(base_url, URI::RFC2396_PARSER.unescape(result[:link]), nil, open_timeout: open_timeout, read_timeout: read_timeout)
|
||||
return nil unless response&.code == "200"
|
||||
|
||||
match = response.body.match(GEO_POSITION_PATTERN)
|
||||
return nil unless match
|
||||
|
||||
{
|
||||
"type" => "Feature",
|
||||
"properties" => { "name" => result[:title], "link" => result[:link] },
|
||||
"geometry" => { "type" => "Point", "coordinates" => [match[2].to_f, match[1].to_f] },
|
||||
}
|
||||
end
|
||||
private_class_method :point_feature
|
||||
|
||||
def self.haversine_km(lat1, lng1, lat2, lng2)
|
||||
earth_radius_km = 6371.0
|
||||
dlat = (lat2 - lat1) * Math::PI / 180
|
||||
dlng = (lng2 - lng1) * Math::PI / 180
|
||||
a = (Math.sin(dlat / 2)**2) + (Math.cos(lat1 * Math::PI / 180) * Math.cos(lat2 * Math::PI / 180) * (Math.sin(dlng / 2)**2))
|
||||
2 * earth_radius_km * Math.asin(Math.sqrt(a))
|
||||
end
|
||||
private_class_method :haversine_km
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user