diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | OsmAndBuilder/OsmAndBuilder.ts | 24 | ||||
| -rw-r--r-- | OsmAndBuilder/index.ts | 2 | ||||
| -rw-r--r-- | OsmAndBuilder/models/OsmAndPoint.ts | 42 | ||||
| -rw-r--r-- | main.js | 188 | ||||
| -rw-r--r-- | package-lock.json | 112 | ||||
| -rw-r--r-- | package.json | 6 |
7 files changed, 377 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..851cbd6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ + +*.gpx diff --git a/OsmAndBuilder/OsmAndBuilder.ts b/OsmAndBuilder/OsmAndBuilder.ts new file mode 100644 index 0000000..9c0930a --- /dev/null +++ b/OsmAndBuilder/OsmAndBuilder.ts @@ -0,0 +1,24 @@ +import { BaseBuilder } from 'gpx-builder'; +import { OsmAndPoint } from './models/OsmAndPoint.ts'; + +export class OsmAndBuilder extends BaseBuilder { + public static MODELS = { + ...BaseBuilder.MODELS, + Point: OsmAndPoint, + }; + + /** + * OsmAnd builder includes extensions for waypoint customization: + * https://osmand.net/docs/technical/osmand-file-formats/osmand-gpx/#waypoints-customization + */ + public constructor() { + super(); + this.data = { + ...this.data, + attributes: { + ...this.data.attributes, + 'xmlns:osmand': 'https://osmand.net/docs/technical/osmand-file-formats/osmand-gpx', + }, + }; + } +} diff --git a/OsmAndBuilder/index.ts b/OsmAndBuilder/index.ts new file mode 100644 index 0000000..8ff9833 --- /dev/null +++ b/OsmAndBuilder/index.ts @@ -0,0 +1,2 @@ +import { OsmAndBuilder } from './OsmAndBuilder.ts'; +export { OsmAndBuilder }; diff --git a/OsmAndBuilder/models/OsmAndPoint.ts b/OsmAndBuilder/models/OsmAndPoint.ts new file mode 100644 index 0000000..a051205 --- /dev/null +++ b/OsmAndBuilder/models/OsmAndPoint.ts @@ -0,0 +1,42 @@ +import { BaseBuilder } from 'gpx-builder'; +const { Point, PointOptions } = BaseBuilder.MODELS; + +export interface OsmAndPointOptions extends PointOptions { + atemp?: number; + bearing?: number; + cad?: number; + course?: number; + depth?: number; + hr?: number; + speed?: number; + wtemp?: number; +} + +export class OsmAndPoint extends Point { + /** + * Extended garmin point. + * + * @see https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd + */ + + public constructor( + lat: number, + lon: number, + options: OsmAndPointOptions = {}, + ) { + super(lat, lon, options); + const { background, color, icon } = options; + + const ext = 'osmand'; + const data = { + ...(typeof background === 'string' ? { [`${ext}:background`]: background } : {}), + ...(typeof color === 'string' ? { [`${ext}:color`]: color } : {}), + ...(typeof icon === 'string' ? { [`${ext}:icon`]: icon } : {}), + }; + + this.extensions = { + ...this.extensions, + ...(Object.keys(data).length > 0 ? data : {}), + }; + } +} @@ -0,0 +1,188 @@ +import { buildGPX } from 'gpx-builder'; +import { OsmAndBuilder } from './OsmAndBuilder/index.ts'; +const { Link, Point } = OsmAndBuilder.MODELS; + +// icons from https://github.com/osmandapp/OsmAnd-resources/tree/master/icons +const ICONS_MAPPING = { + 'src/images/appel22.png': 'shop_greengrocer', + 'src/images/braamboos22.png': 'scrub', + 'src/images/fruit22groen.png': 'vineyard', + 'src/images/garnaal22groen.png': 'reef', // or shop_seafood + 'src/images/noten22groen.png': 'nuts', + 'src/images/overig22groen.png': 'village_green', + 'src/images/paddo22.png': 'military_nuclear_explosion_site', + 'src/images/paddo22groen.png': 'military_nuclear_explosion_site', + 'src/images/peer22.png': 'shop_greengrocer', + 'src/images/planten22groen.png': 'cannabis', // or herbalist + 'src/images/schelp22.png': 'reef', // or shop_seafood + 'src/images/walnoot22.png': 'nuts', +}; +const COLOR_MAPPING_GROUPS = { + 'src/images/garnaal22groen.png': '#f55a52', + 'src/images/noten22groen.png': '#c89760', + 'src/images/overig22groen.png': '#888888', + 'src/images/paddo22.png': '#e2b973', + 'src/images/paddo22groen.png': '#e2b973', + 'src/images/planten22groen.png': '#72a35a', + 'src/images/schelp22.png': '#f55a52', + 'src/images/walnoot22.png': '#c89760', +}; +const COLOR_MAPPING_NAMES = { + 'aalbes': '#da3939', + 'aardbei': '#cf0e08', + 'Amerikaanse_kornoelje': '#e8e9e5', + 'appel': '#da233d', + 'bosbes': '#5a637f', + 'braam': '#060910', + 'druif': '#95b2e2', + 'duindoorn': '#f08916', + 'framboos': '#e53351', + 'gele_kornoelje': '#dfc428', + 'Japanse_Kornoelje': '#dee0e5', + 'jeverbes': '#5f7b98', + 'kers': '#853149', + 'kiwi': '#8bbb2b', + 'krentenboom': '#6d659a', + 'lampionplant': '#f59317', + 'Lijsterbes': '#f35436', + 'linde': '#a0d031', + 'mirabellen': '#e0b630', + 'mispel': '#c76f42', + 'Moerbei': '#19172b', + 'papiermoerbei': '#e12f20', + 'peer': '#b6bd33', + 'pruim': '#955e87', + 'rode_bes': '#da3939', + 'rozenbottel': '#ea1b31', + 'sleedoorn': '#7e93bf', + 'vijg': '#7b6178', + 'vlierbes': '#0e0d12', + 'vossenbes': '#f21900', +}; + +let icons = {}; +let colors = {}; + +fetch('https://app.wildplukwijzer.nl/leafle.js') + .then(response => { + if (!response.ok) + throw new Error (`Failed to retrieve Wildplukwijzer data (${response.status})`); + return response.text(); + }) + .then(response => { + response = response + .split('\n') + .filter(line => line.match(/ = new markerIcon/)) + .map(line => line + .replace(/^(var )?icon_/, 'icons[\'') + .replace(' = new markerIcon({ iconUrl: ', '\'] = ') + .replace(' })', '')) + .join('\n'); + eval(response); + for (let k in icons) { + colors[k] = k in COLOR_MAPPING_NAMES ? COLOR_MAPPING_NAMES[k] : COLOR_MAPPING_GROUPS[icons[k]]; + icons[k] = ICONS_MAPPING[icons[k]]; + } + }).then(() => +fetch('https://app.wildplukwijzer.nl/?hideheader=true')) + .then(response => { + if (!response.ok) + throw new Error (`Failed to retrieve Wildplukwijzer data (${response.status})`); + return response.text(); + }) + .then(response => { + // Extract the relevant JavaScript code from the response + response = response.split('\n'); + let line = null; + do line = response.shift(); while (!line.match(/let AllMarkers = /)); + + // A Leaflet shim with the functions expected by the code we'll extract + let L = { + layerGroup: () => [], + marker: (latlng, settings) => { + if (!settings.icon) + throw new Error(`unknown icon ${settings.icon}`); + let obj = { + lat: latlng[0], + lng: latlng[1], + icon: settings.icon, + }; + obj.addTo = function (soort) { + soort = soort.replace(/ /g, '_'); + if (!(soort in colors)) + throw new Error(`${soort} not in colors`); + obj.color = colors[soort]; + return obj; + }; + obj.bindPopup = function (desc, settings) { + obj.name = desc + .replace(/^.*<h1>(.*)<\/h1>.*$/s, '$1') + .replace(/^./, c => c.toUpperCase()) + .replace(/_/g, ' '); + + obj.type = obj.name; + + obj.img = desc + .replace(/^.*<img src="(.*?)".*$/s, 'http://app.wildplukwijzer.nl/$1'); + + desc = desc + .replace(/.*<\/h1>/s, '') + .replace('<h3>Informatie over de plukplek</h3><p></p>', '') + .replace(/<img src="/g, '<img width="100%" src="http://app.wildplukwijzer.nl/') + .replace(/<h3>/g, '<b>').replace(/<\/h3>/g, ':</b>\n') + .replace(/<\/?div[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/á/g, 'á').replace(/à/, 'à').replace(/ä/g, 'ä') + .replace(/é/g, 'é').replace(/è/, 'è').replace(/ë/g, 'ë') + .replace(/í/g, 'í').replace(/ì/, 'ì').replace(/ï/g, 'ï') + .trim(); + obj.desc = desc; + return obj; + }; + return obj; + } + }; + let AllMarkers = []; + let SoortObject = {}; + + // In setup we create a list of species in SoortObject; each of these also + // needs an icon in the global namespace. + let setup = line.replace('<script>let AllMarkers = [];let SoortObject = {};', ''); + eval(setup); + + // The remainder of this script adds the markers (finding places). We are + // not interested in the click event code, it is the same for each marker. + let markers = []; + line = response.shift(); + while (!line.match(/<\/script>/)) { + markers.push(line + .replace(/icon_(\w*)/, 'icons[\'$1\']') + .replace(/SoortObject\[('[^']*')\]/, '$1')); + line = response.shift(); + } + markers = markers + .join('\n') + .replace(/\.on\('click'.*?\n\s*\)/gs, ''); + eval(markers); + //console.log(AllMarkers); + + // Create waypoints for the markers + let points = []; + AllMarkers.map((m) => { + points.push(new Point(m.lat, m.lng, + { + desc: m.desc, + name: m.name, + type: m.type, + src: 'Wildplukwijzer', + link: new Link(m.img), + icon: m.icon, + color: m.color, + })); + }); + + // Create the GPX + const gpxData = new OsmAndBuilder(); + gpxData.setWayPoints(points); + console.log(buildGPX(gpxData.toObject())); + }); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..eef41e5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,112 @@ +{ + "name": "wildplukwijzer-gpx", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "gpx-builder": "^6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@oozcitak/dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", + "license": "MIT", + "dependencies": { + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", + "license": "MIT", + "engines": { + "node": ">=20.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/gpx-builder": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gpx-builder/-/gpx-builder-6.0.0.tgz", + "integrity": "sha512-BEy6OsO7QUptMeHFx21BpggcB0kShX5OaNcSyXq9khBN3t7K71NyRSkAfRk3GJOGZxUzFxcw6jeYQBjxKp/G5g==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.23.6", + "xmlbuilder2": "^4.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/xmlbuilder2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8d0ecdd --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "gpx-builder": "^6.0.0" + }, + "type": "module" +} |
