Compare commits

...

6 commits
0.7 ... main

Author SHA1 Message Date
777b14641f
add contact address 2024-06-25 10:48:31 +02:00
51d1f5c4cc
add michal bryxí as contributor 2024-06-25 10:46:19 +02:00
bc82090b9b
add zip to flake 2024-06-24 11:18:44 +02:00
6e91a15ca7
Sanitize filenames of downloads.
Some routes have characters like ":" in them which can be illegal on
certain filesystems. Remove the known common ones and replace them with
an underscore.
2024-06-24 11:07:53 +02:00
59d0f01f87
add simple nix flake for development 2024-06-24 10:25:07 +02:00
1fdd4e71b5
departure point can be null 2024-02-16 11:14:32 +01:00
5 changed files with 198 additions and 61 deletions

View file

@ -1,23 +1,33 @@
# SAC Route Portal GPX Downloader # SAC Route Portal GPX Downloader
The [Swiss Alpine Club](https://www.sac-cas.ch/en/) has a great The [Swiss Alpine Club](https://www.sac-cas.ch/en/) has a great
[route portal](https://www.sac-cas.ch/en/huts-and-tours/sac-route-portal/) [route portal](https://www.sac-cas.ch/en/huts-and-tours/sac-route-portal/) for
for finding interesting hiking routes. finding interesting hiking routes.
However you can not download the tracks as gpx files, as they argue that the exact However you can not download the tracks as gpx files, as they argue that the
paths are subject to change and it could be dangerous to adhere too closely to them. exact paths are subject to change and it could be dangerous to adhere too
closely to them.
I do agree with that, but you can draw your own routes I do agree with that, but you can draw your own routes and download those,
and download those, mainly taking some time to draw after the route they are mainly taking some time to draw after the route they are already showing on the
already showing on the map. map.
With this extension you can select any track and then download it as a gpx file With this extension you can select any track and then download it as a gpx file
(an active subscription is needed). (an active subscription is needed).
## Be Cautious! ## Be Cautious!
Heed their warning and do not blindly follow the gps track! Use your brain and Heed their warning and do not blindly follow the gps track! Use your brain and
plan your route beforehand to be aware of dangerous parts and alternatives. plan your route beforehand to be aware of dangerous parts and alternatives.
Take a look at their [safety instructions](https://www.sac-cas.ch/en/training-and-safety/safety/). Take a look at their
[safety instructions](https://www.sac-cas.ch/en/training-and-safety/safety/).
## Contributors ## Contributors
- wizche - wizche
- [Michal Bryxí](mailto:michal.bryxi@gmail.com)
## Contact
For issue reporting contact me at [shu+gpx@vanwa.ch](mailto:shu+gpx@vanwa.ch)

View file

@ -17,26 +17,28 @@ let gpxTrack = null;
*/ */
function toWGS84(point) { function toWGS84(point) {
// convert LV95 into the civilian system // convert LV95 into the civilian system
let y_aux = (point[0] - 2600000) / 1000000; const y_aux = (point[0] - 2600000) / 1000000;
let x_aux = (point[1] - 1200000) / 1000000; const x_aux = (point[1] - 1200000) / 1000000;
// calculate longitude and latitude in the unit 10000" // calculate longitude and latitude in the unit 10000"
let lat = 16.9023892 + let lat =
16.9023892 +
3.238272 * x_aux - 3.238272 * x_aux -
0.270978 * Math.pow(y_aux, 2) - 0.270978 * Math.pow(y_aux, 2) -
0.002528 * Math.pow(x_aux, 2) - 0.002528 * Math.pow(x_aux, 2) -
0.0447 * Math.pow(y_aux, 2) * x_aux - 0.0447 * Math.pow(y_aux, 2) * x_aux -
0.0140 * Math.pow(x_aux, 3); 0.014 * Math.pow(x_aux, 3);
let lon = 2.6779094 + let lon =
2.6779094 +
4.728982 * y_aux + 4.728982 * y_aux +
0.791484 * y_aux * x_aux + 0.791484 * y_aux * x_aux +
0.1306 * y_aux * Math.pow(x_aux, 2) - 0.1306 * y_aux * Math.pow(x_aux, 2) -
0.0436 * Math.pow(y_aux, 3); 0.0436 * Math.pow(y_aux, 3);
// unit 10000" to 1" and seconds to degrees (dec) // unit 10000" to 1" and seconds to degrees (dec)
lat = lat * 100 / 36; lat = (lat * 100) / 36;
lon = lon * 100 / 36; lon = (lon * 100) / 36;
return { lat: lat, lon: lon }; return { lat: lat, lon: lon };
} }
@ -48,7 +50,7 @@ function toWGS84(point) {
* @returns Track point xml node for gpx. * @returns Track point xml node for gpx.
*/ */
function toTrackPoint(point) { function toTrackPoint(point) {
let wgs84Point = toWGS84(point); const wgs84Point = toWGS84(point);
return `<trkpt lat="${wgs84Point.lat}" lon="${wgs84Point.lon}"/>`; return `<trkpt lat="${wgs84Point.lat}" lon="${wgs84Point.lon}"/>`;
} }
@ -58,7 +60,7 @@ function toTrackPoint(point) {
* @returns Way point xml node for gpx. * @returns Way point xml node for gpx.
*/ */
function toWayPoint(point) { function toWayPoint(point) {
let wgs84Point = toWGS84(point.geom.coordinates); const wgs84Point = toWGS84(point.geom.coordinates);
return ` return `
<wpt lat="${wgs84Point.lat}" lon="${wgs84Point.lon}"> <wpt lat="${wgs84Point.lat}" lon="${wgs84Point.lon}">
<ele>${point.altitude}</ele> <ele>${point.altitude}</ele>
@ -73,7 +75,6 @@ function toWayPoint(point) {
* @returns Combined route number, id and route title. * @returns Combined route number, id and route title.
*/ */
function trackTitle(geoJson) { function trackTitle(geoJson) {
const route = geoJson.segments[0];
const book = geoJson.book_route_number const book = geoJson.book_route_number
? `${geoJson.book_route_number} - ` ? `${geoJson.book_route_number} - `
: ""; : "";
@ -88,18 +89,25 @@ function trackTitle(geoJson) {
* @returns Simple gpx string. * @returns Simple gpx string.
*/ */
function toGpx(geoJson) { function toGpx(geoJson) {
let trackSegments = geoJson.segments.map((segment) => { const trackSegments = geoJson.segments
if (segment.geom == null) return ""; .map((segment) => {
return `<trkseg> if (segment.geom == null) return "";
return `<trkseg>
${segment.geom.coordinates.map(toTrackPoint).join("")} ${segment.geom.coordinates.map(toTrackPoint).join("")}
</trkseg>`; </trkseg>`;
}).join(""); })
.join("");
let endPoint = geoJson.end_point ? toWayPoint(geoJson.end_point) : ""; const departurePoint = geoJson.departure_point
let waypoints = geoJson.waypoints ? toWayPoint(geoJson.departure_point)
? geoJson.waypoints.map((wp) => { : "";
return toWayPoint(wp.reference_poi); const endPoint = geoJson.end_point ? toWayPoint(geoJson.end_point) : "";
}).join("") const waypoints = geoJson.waypoints
? geoJson.waypoints
.map((wp) => {
return toWayPoint(wp.reference_poi);
})
.join("")
: ""; : "";
const routeTitle = trackTitle(geoJson); const routeTitle = trackTitle(geoJson);
@ -111,7 +119,7 @@ function toGpx(geoJson) {
xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd" xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd"
version="1.0" version="1.0"
creator="SAC-Tourenportal GPX Downloader"> creator="SAC-Tourenportal GPX Downloader">
${toWayPoint(geoJson.departure_point)} ${departurePoint}
${toWayPoint(geoJson.destination_poi)} ${toWayPoint(geoJson.destination_poi)}
${waypoints} ${waypoints}
${endPoint} ${endPoint}
@ -123,7 +131,7 @@ function toGpx(geoJson) {
`; `;
const parser = new DOMParser(); const parser = new DOMParser();
let xmlDoc = parser.parseFromString(xmlString, "text/xml"); const xmlDoc = parser.parseFromString(xmlString, "text/xml");
return new XMLSerializer().serializeToString(xmlDoc.documentElement); return new XMLSerializer().serializeToString(xmlDoc.documentElement);
} }
@ -132,26 +140,26 @@ function toGpx(geoJson) {
* Intercept the download of GeoJSON data and save it for the background script. * Intercept the download of GeoJSON data and save it for the background script.
*/ */
function listener(details) { function listener(details) {
let filter = browser.webRequest.filterResponseData(details.requestId); const filter = browser.webRequest.filterResponseData(details.requestId);
let decoder = new TextDecoder("utf-8"); const decoder = new TextDecoder("utf-8");
let encoder = new TextEncoder(); const encoder = new TextEncoder();
let data = []; const data = [];
filter.ondata = (event) => { filter.ondata = (event) => {
data.push(event.data); data.push(event.data);
}; };
filter.onstop = async (event) => { filter.onstop = async (_event) => {
let blob = new Blob(data, { type: "text/html" }); const blob = new Blob(data, { type: "text/html" });
let buffer = await blob.arrayBuffer(); const buffer = await blob.arrayBuffer();
let str = decoder.decode(buffer); const str = decoder.decode(buffer);
updateActiveTab(browser.tabs); updateActiveTab(browser.tabs);
filter.write(encoder.encode(str)); filter.write(encoder.encode(str));
filter.close(); filter.close();
let geoJson = JSON.parse(str); const geoJson = JSON.parse(str);
const routeTitle = trackTitle(geoJson); const routeTitle = trackTitle(geoJson);
gpxTrack = { title: routeTitle, data: toGpx(geoJson) }; gpxTrack = { title: routeTitle, data: toGpx(geoJson) };
}; };
@ -173,6 +181,41 @@ function checkTrack(tab) {
return tab.url.match("https://www.sac-cas.ch/.*") && gpxTrack; 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. * If a valid tack was selected, download it as a gpx file.
*/ */
@ -182,12 +225,13 @@ function handleClick(tab) {
return; return;
} }
let blob = new Blob([gpxTrack.data], { type: "application/gpx+xml" }); const blob = new Blob([gpxTrack.data], { type: "application/gpx+xml" });
let objectURL = URL.createObjectURL(blob); const objectURL = URL.createObjectURL(blob);
let downloading = browser.downloads.download({ const filename = sanitizeFilename(gpxTrack.title);
const downloading = browser.downloads.download({
url: objectURL, url: objectURL,
filename: `${gpxTrack.title}.gpx`, filename: `${filename}.gpx`,
saveAs: true, saveAs: true,
conflictAction: "uniquify", conflictAction: "uniquify",
}); });
@ -202,14 +246,14 @@ function handleClick(tab) {
/** /**
* Update the download icon and text. * Update the download icon and text.
*/ */
function updateActiveTab(tabs) { function updateActiveTab(_tabs) {
function updateTab(tabs) { function updateTab(tabs) {
if (tabs[0]) { if (tabs[0]) {
updateIcon(tabs[0]); updateIcon(tabs[0]);
} }
} }
let gettingActiveTab = browser.tabs.query({ const gettingActiveTab = browser.tabs.query({
active: true, active: true,
currentWindow: true, currentWindow: true,
}); });
@ -225,11 +269,11 @@ function updateIcon(tab) {
browser.browserAction.setIcon({ browser.browserAction.setIcon({
path: hasTrack path: hasTrack
? { ? {
48: "icons/map.png", 48: "icons/map.png",
} }
: { : {
48: "icons/map-disabled.png", 48: "icons/map-disabled.png",
}, },
tabId: tab.id, tabId: tab.id,
}); });
browser.browserAction.setTitle({ browser.browserAction.setTitle({

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1719075281,
"narHash": "sha256-CyyxvOwFf12I91PBWz43iGT1kjsf5oi6ax7CrvaMyAo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a71e967ef3694799d0c418c98332f7ff4cc5f6af",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

26
flake.nix Normal file
View file

@ -0,0 +1,26 @@
{
description = "little-hesinde project";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
devShells.default =
with pkgs;
mkShell {
buildInputs = [
deno
zip
];
};
}
);
}

View file

@ -1,19 +1,15 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "SAC Route Portal GPX Downloader", "name": "SAC Route Portal GPX Downloader",
"version": "0.7", "version": "0.9",
"developer": { "developer": {
"name": "Sebastian Hugentobler", "name": "Sebastian Hugentobler",
"url": "https://code.vanwa.ch/sebastian/sac-route-portal-gpx-fx" "url": "https://code.vanwa.ch/sebastian/sac-route-portal-gpx-fx"
}, },
"description": "Download gpx tracks from the sac route portal.", "description": "Download gpx tracks from the sac route portal.",
"icons": { "icons": {
"48": "icons/map.png" "48": "icons/map.png"
}, },
"permissions": [ "permissions": [
"activeTab", "activeTab",
"downloads", "downloads",
@ -21,13 +17,13 @@
"webRequestBlocking", "webRequestBlocking",
"https://www.sac-cas.ch/*" "https://www.sac-cas.ch/*"
], ],
"background": { "background": {
"scripts": ["background.js"] "scripts": [
"background.js"
]
}, },
"browser_action": { "browser_action": {
"default_icon": "icons/map.png", "default_icon": "icons/map.png",
"default_title": "To GPX" "default_title": "To GPX"
} }
} }