/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ let gpxTrack = null; /** * Convert the Swiss projection coordinates to WGS84 (lat/lon). * * Calculations from "Approximate formulas for the transformation between Swiss * projection coordinates and WGS84" by the Swiss Federal Office of Topography, * swisstopo * (https://www.swisstopo.admin.ch/en/knowledge-facts/surveying-geodesy/reference-systems/map-projections.html). * * @param {*} point Array of Swiss projection coordinates, position 0 is E and 1 is N. * @returns Calculated lat and lon. */ function toWGS84(point) { // convert LV95 into the civilian system const y_aux = (point[0] - 2600000) / 1000000; const x_aux = (point[1] - 1200000) / 1000000; // calculate longitude and latitude in the unit 10000" let lat = 16.9023892 + 3.238272 * x_aux - 0.270978 * Math.pow(y_aux, 2) - 0.002528 * Math.pow(x_aux, 2) - 0.0447 * Math.pow(y_aux, 2) * x_aux - 0.014 * Math.pow(x_aux, 3); let lon = 2.6779094 + 4.728982 * y_aux + 0.791484 * y_aux * x_aux + 0.1306 * y_aux * Math.pow(x_aux, 2) - 0.0436 * Math.pow(y_aux, 3); // unit 10000" to 1" and seconds to degrees (dec) lat = (lat * 100) / 36; lon = (lon * 100) / 36; return { lat: lat, lon: lon }; } /** * Get the gpx trackpoint representation of a Swiss projection coordinate point. * * @param {*} point Array of Swiss projection coordinates, position 0 is E and 1 is N. * @returns Track point xml node for gpx. */ function toTrackPoint(point) { const wgs84Point = toWGS84(point); return ``; } /** * Get the gpx waypoint representation of a route portal point. * * @returns Way point xml node for gpx. */ function toWayPoint(point) { const wgs84Point = toWGS84(point.geom.coordinates); return ` ${point.altitude} ${point.display_name} `; } /** * Create track title. * * @param {*} geoJson * @returns Combined route number, id and route title. */ function trackTitle(geoJson) { const book = geoJson.book_route_number ? `${geoJson.book_route_number} - ` : ""; return `${book}${geoJson.title}`; } /** * Create a gpx representation from the sac GeoJSON data. * * @param {*} geoJson * @returns Simple gpx string. */ function toGpx(geoJson) { const trackSegments = geoJson.segments .map((segment) => { if (segment.geom == null) return ""; return ` ${segment.geom.coordinates.map(toTrackPoint).join("")} `; }) .join(""); const departurePoint = geoJson.departure_point ? toWayPoint(geoJson.departure_point) : ""; const endPoint = geoJson.end_point ? toWayPoint(geoJson.end_point) : ""; const waypoints = geoJson.waypoints ? geoJson.waypoints .map((wp) => { return toWayPoint(wp.reference_poi); }) .join("") : ""; const routeTitle = trackTitle(geoJson); const xmlString = ` ${departurePoint} ${toWayPoint(geoJson.destination_poi)} ${waypoints} ${endPoint} Track ${routeTitle} ${trackSegments} `; const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "text/xml"); return new XMLSerializer().serializeToString(xmlDoc.documentElement); } /** * Intercept the download of GeoJSON data and save it for the background script. */ function listener(details) { const filter = browser.webRequest.filterResponseData(details.requestId); const decoder = new TextDecoder("utf-8"); const encoder = new TextEncoder(); const data = []; filter.ondata = (event) => { data.push(event.data); }; filter.onstop = async (_event) => { const blob = new Blob(data, { type: "text/html" }); const buffer = await blob.arrayBuffer(); const str = decoder.decode(buffer); updateActiveTab(browser.tabs); filter.write(encoder.encode(str)); filter.close(); const geoJson = JSON.parse(str); const routeTitle = trackTitle(geoJson); gpxTrack = { title: routeTitle, data: toGpx(geoJson) }; }; return {}; } /** * Check if a url should be intercepted. * * @param {*} tab * @returns True if the active tab is an sac website, false otherwise. */ function checkTrack(tab) { if (!tab.url) { return false; } return tab.url.match("https://www.sac-cas.ch/.*") && gpxTrack; } /** * Remove characters that lead to problems in common filesystems. * * Windows (NTFS) seems to have the most restrictions, so it is mostly sourced from there. * * Reserved names on windows: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file * * Code comes from https://github.com/parshap/node-sanitize-filename/blob/master/index.js */ function sanitizeFilename(input) { const illegal = /[\/\?<>\\:\*\|"]/g; // deno-lint-ignore no-control-regex const control = /[\x00-\x1f\x80-\x9f]/g; const reserved = /^\.+$/; const windowsReserved = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; const windowsTrailing = /[\. ]+$/; const replacement = "_"; const sanitized = input .replace(illegal, replacement) .replace(control, replacement) .replace(reserved, replacement) .replace(windowsReserved, replacement) .replace(windowsTrailing, replacement); if (sanitized.length === 0) { sanitized = "unnamed"; } const maxLength = 250; const uint8Array = new TextEncoder().encode(sanitized); const truncated = uint8Array.slice(0, maxLength); return new TextDecoder().decode(truncated); } /** * If a valid tack was selected, download it as a gpx file. */ function handleClick(tab) { const hasTrack = checkTrack(tab); if (!hasTrack) { return; } const blob = new Blob([gpxTrack.data], { type: "application/gpx+xml" }); const objectURL = URL.createObjectURL(blob); const filename = sanitizeFilename(gpxTrack.title); const downloading = browser.downloads.download({ url: objectURL, filename: `${filename}.gpx`, saveAs: true, conflictAction: "uniquify", }); downloading.then( (id) => console.log(`Started downloading: ${id}`), (error) => console.log(`Download failed: ${error}`), ); gpxTrack = null; } /** * Update the download icon and text. */ function updateActiveTab(_tabs) { function updateTab(tabs) { if (tabs[0]) { updateIcon(tabs[0]); } } const gettingActiveTab = browser.tabs.query({ active: true, currentWindow: true, }); gettingActiveTab.then(updateTab); } /** * Update the download icon. */ function updateIcon(tab) { const hasTrack = checkTrack(tab); browser.browserAction.setIcon({ path: hasTrack ? { 48: "icons/map.png", } : { 48: "icons/map-disabled.png", }, tabId: tab.id, }); browser.browserAction.setTitle({ title: hasTrack ? `Download track "${gpxTrack.title}"` : "No track selected", tabId: tab.id, }); } browser.webRequest.onBeforeRequest.addListener( listener, { urls: ["https://www.sac-cas.ch/*[routeId]*"] }, ["blocking"], ); browser.browserAction.onClicked.addListener(handleClick); browser.tabs.onUpdated.addListener(updateActiveTab); browser.tabs.onActivated.addListener(updateActiveTab); browser.windows.onFocusChanged.addListener(updateActiveTab); updateActiveTab();