2021-06-15 09:14:46 +00:00
|
|
|
/* 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/. */
|
2021-06-16 08:29:25 +00:00
|
|
|
|
2021-06-15 09:14:46 +00:00
|
|
|
let gpxTrack = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert the Swiss projection coordinates to WGS84 (lat/lon).
|
2023-02-01 19:37:40 +00:00
|
|
|
*
|
|
|
|
* Calculations from "Approximate formulas for the transformation between Swiss
|
|
|
|
* projection coordinates and WGS84" by the Swiss Federal Office of Topography,
|
|
|
|
* swisstopo
|
2021-06-15 09:14:46 +00:00
|
|
|
* (https://www.swisstopo.admin.ch/en/knowledge-facts/surveying-geodesy/reference-systems/map-projections.html).
|
2023-02-01 19:37:40 +00:00
|
|
|
*
|
2021-06-15 09:14:46 +00:00
|
|
|
* @param {*} point Array of Swiss projection coordinates, position 0 is E and 1 is N.
|
2021-06-16 08:29:25 +00:00
|
|
|
* @returns Calculated lat and lon.
|
2021-06-15 09:14:46 +00:00
|
|
|
*/
|
2021-06-16 08:29:25 +00:00
|
|
|
function toWGS84(point) {
|
2023-02-01 19:37:40 +00:00
|
|
|
// convert LV95 into the civilian system
|
2024-02-16 10:14:32 +00:00
|
|
|
const y_aux = (point[0] - 2600000) / 1000000;
|
|
|
|
const x_aux = (point[1] - 1200000) / 1000000;
|
2023-02-01 19:37:40 +00:00
|
|
|
|
|
|
|
// calculate longitude and latitude in the unit 10000"
|
2024-06-24 09:07:53 +00:00
|
|
|
let lat =
|
|
|
|
16.9023892 +
|
2023-02-01 19:37:40 +00:00
|
|
|
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 -
|
2024-02-16 10:14:32 +00:00
|
|
|
0.014 * Math.pow(x_aux, 3);
|
2023-02-01 19:37:40 +00:00
|
|
|
|
2024-06-24 09:07:53 +00:00
|
|
|
let lon =
|
|
|
|
2.6779094 +
|
2023-02-01 19:37:40 +00:00
|
|
|
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)
|
2024-02-16 10:14:32 +00:00
|
|
|
lat = (lat * 100) / 36;
|
|
|
|
lon = (lon * 100) / 36;
|
2023-02-01 19:37:40 +00:00
|
|
|
|
|
|
|
return { lat: lat, lon: lon };
|
2021-06-16 08:29:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the gpx trackpoint representation of a Swiss projection coordinate point.
|
2023-02-01 19:37:40 +00:00
|
|
|
*
|
2021-06-16 08:29:25 +00:00
|
|
|
* @param {*} point Array of Swiss projection coordinates, position 0 is E and 1 is N.
|
|
|
|
* @returns Track point xml node for gpx.
|
|
|
|
*/
|
2023-02-01 19:37:40 +00:00
|
|
|
function toTrackPoint(point) {
|
2024-02-16 10:14:32 +00:00
|
|
|
const wgs84Point = toWGS84(point);
|
2023-02-01 19:37:40 +00:00
|
|
|
return `<trkpt lat="${wgs84Point.lat}" lon="${wgs84Point.lon}"/>`;
|
2021-06-16 08:29:25 +00:00
|
|
|
}
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2021-06-16 08:29:25 +00:00
|
|
|
/**
|
|
|
|
* Get the gpx waypoint representation of a route portal point.
|
2023-02-01 19:37:40 +00:00
|
|
|
*
|
2021-06-16 08:29:25 +00:00
|
|
|
* @returns Way point xml node for gpx.
|
|
|
|
*/
|
2023-02-01 19:37:40 +00:00
|
|
|
function toWayPoint(point) {
|
2024-02-16 10:14:32 +00:00
|
|
|
const wgs84Point = toWGS84(point.geom.coordinates);
|
2023-02-01 19:37:40 +00:00
|
|
|
return `
|
2021-06-16 08:29:25 +00:00
|
|
|
<wpt lat="${wgs84Point.lat}" lon="${wgs84Point.lon}">
|
|
|
|
<ele>${point.altitude}</ele>
|
|
|
|
<name>${point.display_name}</name>
|
|
|
|
</wpt>`;
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create track title.
|
2023-02-01 19:37:40 +00:00
|
|
|
*
|
|
|
|
* @param {*} geoJson
|
2021-06-15 09:14:46 +00:00
|
|
|
* @returns Combined route number, id and route title.
|
|
|
|
*/
|
|
|
|
function trackTitle(geoJson) {
|
2023-02-01 19:37:40 +00:00
|
|
|
const book = geoJson.book_route_number
|
|
|
|
? `${geoJson.book_route_number} - `
|
|
|
|
: "";
|
2021-06-16 10:59:13 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
return `${book}${geoJson.title}`;
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a gpx representation from the sac GeoJSON data.
|
2023-02-01 19:37:40 +00:00
|
|
|
*
|
|
|
|
* @param {*} geoJson
|
2021-06-15 09:14:46 +00:00
|
|
|
* @returns Simple gpx string.
|
|
|
|
*/
|
|
|
|
function toGpx(geoJson) {
|
2024-02-16 10:14:32 +00:00
|
|
|
const trackSegments = geoJson.segments
|
|
|
|
.map((segment) => {
|
|
|
|
if (segment.geom == null) return "";
|
|
|
|
return `<trkseg>
|
2023-02-03 11:41:49 +00:00
|
|
|
${segment.geom.coordinates.map(toTrackPoint).join("")}
|
|
|
|
</trkseg>`;
|
2024-02-16 10:14:32 +00:00
|
|
|
})
|
|
|
|
.join("");
|
2021-06-16 10:42:39 +00:00
|
|
|
|
2024-02-16 10:14:32 +00:00
|
|
|
const departurePoint = geoJson.departure_point
|
|
|
|
? toWayPoint(geoJson.departure_point)
|
|
|
|
: "";
|
|
|
|
const endPoint = geoJson.end_point ? toWayPoint(geoJson.end_point) : "";
|
|
|
|
const waypoints = geoJson.waypoints
|
|
|
|
? geoJson.waypoints
|
2024-06-24 09:07:53 +00:00
|
|
|
.map((wp) => {
|
|
|
|
return toWayPoint(wp.reference_poi);
|
|
|
|
})
|
|
|
|
.join("")
|
2023-02-01 19:37:40 +00:00
|
|
|
: "";
|
2021-06-16 10:59:13 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
const routeTitle = trackTitle(geoJson);
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
const xmlString = `<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
2021-06-15 09:14:46 +00:00
|
|
|
|
|
|
|
<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
|
|
xmlns="http://www.topografix.com/GPX/1/0"
|
|
|
|
xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd"
|
|
|
|
version="1.0"
|
|
|
|
creator="SAC-Tourenportal GPX Downloader">
|
2024-02-16 10:14:32 +00:00
|
|
|
${departurePoint}
|
2021-06-16 08:29:25 +00:00
|
|
|
${toWayPoint(geoJson.destination_poi)}
|
2021-06-16 10:59:13 +00:00
|
|
|
${waypoints}
|
|
|
|
${endPoint}
|
2021-06-15 09:14:46 +00:00
|
|
|
<trk>
|
|
|
|
<name>Track ${routeTitle}</name>
|
2021-06-16 10:42:39 +00:00
|
|
|
${trackSegments}
|
2021-06-15 09:14:46 +00:00
|
|
|
</trk>
|
|
|
|
</gpx>
|
|
|
|
`;
|
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
const parser = new DOMParser();
|
2024-02-16 10:14:32 +00:00
|
|
|
const xmlDoc = parser.parseFromString(xmlString, "text/xml");
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
return new XMLSerializer().serializeToString(xmlDoc.documentElement);
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Intercept the download of GeoJSON data and save it for the background script.
|
|
|
|
*/
|
|
|
|
function listener(details) {
|
2024-02-16 10:14:32 +00:00
|
|
|
const filter = browser.webRequest.filterResponseData(details.requestId);
|
|
|
|
const decoder = new TextDecoder("utf-8");
|
|
|
|
const encoder = new TextEncoder();
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2024-02-16 10:14:32 +00:00
|
|
|
const data = [];
|
2023-02-01 19:37:40 +00:00
|
|
|
filter.ondata = (event) => {
|
|
|
|
data.push(event.data);
|
|
|
|
};
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2024-02-16 10:14:32 +00:00
|
|
|
filter.onstop = async (_event) => {
|
|
|
|
const blob = new Blob(data, { type: "text/html" });
|
|
|
|
const buffer = await blob.arrayBuffer();
|
|
|
|
const str = decoder.decode(buffer);
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
updateActiveTab(browser.tabs);
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
filter.write(encoder.encode(str));
|
|
|
|
filter.close();
|
2021-06-16 10:02:26 +00:00
|
|
|
|
2024-02-16 10:14:32 +00:00
|
|
|
const geoJson = JSON.parse(str);
|
2023-02-01 19:37:40 +00:00
|
|
|
const routeTitle = trackTitle(geoJson);
|
|
|
|
gpxTrack = { title: routeTitle, data: toGpx(geoJson) };
|
|
|
|
};
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
return {};
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a url should be intercepted.
|
2023-02-01 19:37:40 +00:00
|
|
|
*
|
|
|
|
* @param {*} tab
|
2021-06-15 09:14:46 +00:00
|
|
|
* @returns True if the active tab is an sac website, false otherwise.
|
|
|
|
*/
|
|
|
|
function checkTrack(tab) {
|
2023-02-01 19:37:40 +00:00
|
|
|
if (!tab.url) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
return tab.url.match("https://www.sac-cas.ch/.*") && gpxTrack;
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
|
|
|
|
2024-06-24 09:07:53 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
2021-06-15 09:14:46 +00:00
|
|
|
/**
|
|
|
|
* If a valid tack was selected, download it as a gpx file.
|
|
|
|
*/
|
|
|
|
function handleClick(tab) {
|
2023-02-01 19:37:40 +00:00
|
|
|
const hasTrack = checkTrack(tab);
|
|
|
|
if (!hasTrack) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-02-16 10:14:32 +00:00
|
|
|
const blob = new Blob([gpxTrack.data], { type: "application/gpx+xml" });
|
|
|
|
const objectURL = URL.createObjectURL(blob);
|
2023-02-01 19:37:40 +00:00
|
|
|
|
2024-06-24 09:07:53 +00:00
|
|
|
const filename = sanitizeFilename(gpxTrack.title);
|
2024-02-16 10:14:32 +00:00
|
|
|
const downloading = browser.downloads.download({
|
2023-02-01 19:37:40 +00:00
|
|
|
url: objectURL,
|
2024-06-24 09:07:53 +00:00
|
|
|
filename: `${filename}.gpx`,
|
2023-02-01 19:37:40 +00:00
|
|
|
saveAs: true,
|
|
|
|
conflictAction: "uniquify",
|
|
|
|
});
|
|
|
|
|
|
|
|
downloading.then(
|
|
|
|
(id) => console.log(`Started downloading: ${id}`),
|
|
|
|
(error) => console.log(`Download failed: ${error}`),
|
|
|
|
);
|
|
|
|
gpxTrack = null;
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the download icon and text.
|
|
|
|
*/
|
2024-02-16 10:14:32 +00:00
|
|
|
function updateActiveTab(_tabs) {
|
2023-02-01 19:37:40 +00:00
|
|
|
function updateTab(tabs) {
|
|
|
|
if (tabs[0]) {
|
|
|
|
updateIcon(tabs[0]);
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
2023-02-01 19:37:40 +00:00
|
|
|
}
|
|
|
|
|
2024-02-16 10:14:32 +00:00
|
|
|
const gettingActiveTab = browser.tabs.query({
|
2023-02-01 19:37:40 +00:00
|
|
|
active: true,
|
|
|
|
currentWindow: true,
|
|
|
|
});
|
|
|
|
gettingActiveTab.then(updateTab);
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the download icon.
|
|
|
|
*/
|
|
|
|
function updateIcon(tab) {
|
2023-02-01 19:37:40 +00:00
|
|
|
const hasTrack = checkTrack(tab);
|
2021-06-15 09:14:46 +00:00
|
|
|
|
2023-02-01 19:37:40 +00:00
|
|
|
browser.browserAction.setIcon({
|
|
|
|
path: hasTrack
|
|
|
|
? {
|
2024-06-24 09:07:53 +00:00
|
|
|
48: "icons/map.png",
|
|
|
|
}
|
2023-02-01 19:37:40 +00:00
|
|
|
: {
|
2024-06-24 09:07:53 +00:00
|
|
|
48: "icons/map-disabled.png",
|
|
|
|
},
|
2023-02-01 19:37:40 +00:00
|
|
|
tabId: tab.id,
|
|
|
|
});
|
|
|
|
browser.browserAction.setTitle({
|
|
|
|
title: hasTrack
|
|
|
|
? `Download track "${gpxTrack.title}"`
|
|
|
|
: "No track selected",
|
|
|
|
tabId: tab.id,
|
|
|
|
});
|
2021-06-15 09:14:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
browser.webRequest.onBeforeRequest.addListener(
|
2023-02-01 19:37:40 +00:00
|
|
|
listener,
|
|
|
|
{ urls: ["https://www.sac-cas.ch/*[routeId]*"] },
|
|
|
|
["blocking"],
|
2021-06-15 09:14:46 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
browser.browserAction.onClicked.addListener(handleClick);
|
|
|
|
|
|
|
|
browser.tabs.onUpdated.addListener(updateActiveTab);
|
|
|
|
browser.tabs.onActivated.addListener(updateActiveTab);
|
|
|
|
browser.windows.onFocusChanged.addListener(updateActiveTab);
|
2023-02-01 19:37:40 +00:00
|
|
|
|
2021-06-15 09:14:46 +00:00
|
|
|
updateActiveTab();
|