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