require "net/http"
require "rexml/document"
module Maptool
# A reachability check plus a passthrough proxy for an external
# kiwix-serve instance serving an offline Wikipedia ZIM (see
# docs/development.adoc for how to set one up). A POI popup's "Nearby on
# Wikipedia" link (public/js/gpx.js) used to point straight at
# kiwix-serve itself — fine on a LAN, broken once this app started
# running behind a reverse proxy (map.priesol.net) that only forwards to
# *this* app, leaving no address a browser could reach kiwix-serve at
# directly. GET /kiwix/* in app.rb calls .proxy below instead.
module Kiwix
# — 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 = / "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