Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
777b14641f | |||
51d1f5c4cc | |||
bc82090b9b | |||
6e91a15ca7 | |||
59d0f01f87 | |||
1fdd4e71b5 |
5 changed files with 198 additions and 61 deletions
34
README.md
34
README.md
|
@ -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
|
|
||||||
[route portal](https://www.sac-cas.ch/en/huts-and-tours/sac-route-portal/)
|
|
||||||
for finding interesting hiking routes.
|
|
||||||
|
|
||||||
However you can not download the tracks as gpx files, as they argue that the exact
|
The [Swiss Alpine Club](https://www.sac-cas.ch/en/) has a great
|
||||||
paths are subject to change and it could be dangerous to adhere too closely to them.
|
[route portal](https://www.sac-cas.ch/en/huts-and-tours/sac-route-portal/) for
|
||||||
|
finding interesting hiking routes.
|
||||||
|
|
||||||
I do agree with that, but you can draw your own routes
|
However you can not download the tracks as gpx files, as they argue that the
|
||||||
and download those, mainly taking some time to draw after the route they are
|
exact paths are subject to change and it could be dangerous to adhere too
|
||||||
already showing on the map.
|
closely to them.
|
||||||
|
|
||||||
With this extension you can select any track and then download it as a gpx file
|
I do agree with that, but you can draw your own routes and download those,
|
||||||
|
mainly taking some time to draw after the route they are already showing on the
|
||||||
|
map.
|
||||||
|
|
||||||
|
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
|
|
||||||
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/).
|
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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
124
background.js
124
background.js
|
@ -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
61
flake.lock
generated
Normal 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
26
flake.nix
Normal 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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue