MediaWiki:Gadget-Poi2gpx.js
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
//<nowiki>
/*********************************************************************
* poi2gpx v1.5, 2024-08-12
* Download of article’s points of interest and tracks to a GPX file
* Original author: Roland Unger
* Support of desktop and mobile views
* Documentation: https://de.wikivoyage.org/wiki/Wikivoyage:Gadget-Poi2gpx.js
* License: GPL-2.0+, CC-by-sa 3.0
*********************************************************************/
/* eslint-disable mediawiki/class-doc */
( function ( $, mw ) {
'use strict';
var poi2gpx = function() {
// technical constants
const minervaPageActionsSelector = '.page-actions-menu #page-actions-edit',
imgSrc = '//upload.wikimedia.org/wikivoyage/de/thumb/5/5f/WV-poi2gpx-green.svg/50px-WV-poi2gpx-green.svg.png',
imgSrcMinerva = '//upload.wikimedia.org/wikivoyage/de/thumb/6/63/WV-poi2gpx-black.svg/50px-WV-poi2gpx-black.svg.png',
// image for download link
allowedNamespaces = [
0, // Main
2, // User
4 // Project
],
commentClasses = [ 'listing-hours', 'listing-checkin', 'listing-checkout',
'listing-price', 'listing-credit' ],
containerClass = 'vcard',
// contains wrapper markup of a single marker or listing
contentClass = 'listing-content',
dataColor = 'data-color',
dataName = 'data-name',
dataPhone = 'data-phone',
dataType = 'data-group-translated', // other wikis: 'data-type'
dataUrl = 'data-url',
fallbackLang = 'en',
kartographerClass = 'mw-kartographer-maplink',
nameClass = 'listing-name',
noGpxClass = 'listing-no-gpx';
// strings depending on page language
// depending on group names defined in Module:Marker utilities/Groups
const wikiStrings = {
de: {
track: 'Track'
},
en: {
track: 'track'
},
es: {
track: 'sendero'
},
fr: {
track: 'piste'
},
it: {
track: 'traccia'
}
};
// strings depending on user language
const userStrings = {
de: {
gpxFileDescr: 'Kartenpositionen aus dem deutschen Wikivoyage-Artikel',
gpxLabel: 'GPX',
gpxTitle: 'Download der Kartenpositionen als GPX-Datei'
},
en: {
gpxFileDescr: 'Map positions from the German Wikivoyage article',
gpxLabel: 'GPX',
gpxTitle: 'Download of the map positions as a GPX file'
},
es: {
gpxFileDescr: 'Posiciones en el mapa del artículo de Wikivoyage en alemán',
gpxLabel: 'GPX',
gpxTitle: 'Descarga de las posiciones del mapa como archivo GPX'
},
fr: {
gpxFileDescr: 'Positions sur la carte de l’article de Wikivoyage allemand',
gpxLabel: 'GPX',
gpxTitle: 'Téléchargement des positions de la carte sous forme de fichier GPX'
},
it: {
gpxFileDescr: 'Posizioni sulla mappa dall’articolo tedesco di Wikivoyage',
gpxLabel: 'GPX',
gpxTitle: 'Download delle posizioni della mappa come file GPX'
}
};
// internal use
const pageLang = mw.config.get( 'wgPageContentLanguage' ),
userLang = mw.config.get( 'wgUserLanguage' ),
pageTitle = mw.config.get( 'wgTitle' ),
isMinerva = mw.config.get( 'skin' ) === 'minerva'; // mobile view
var gpxFile = null, // check for URL object
trackdata = null,
messages = {};
// copying translation strings to messages depending on chain languages
function addMessages( strings, chain ) {
for ( var i = chain.length - 1; i >= 0; i-- ) {
if ( strings.hasOwnProperty( chain[ i ] ) ) {
$.extend( messages, strings[ chain[ i ] ] );
}
}
}
// copying translation strings to messages
function setupMessages() {
addMessages( wikiStrings, [ pageLang, fallbackLang ] );
const chain = ( userLang == pageLang ) ? [ pageLang, fallbackLang ] :
[ userLang, pageLang, fallbackLang ];
addMessages( userStrings, chain );
}
// to use text in XML tags
function replace( text ) {
return text.replace( /\&/g, '&' )
.replace( /"/g, '"' )
.replace( /</g, '<' )
.replace( />/g, '>' );
}
function removeTags( s ) {
return $( '<div>' + s + '</div>' ).text();
}
function getString( prop ) {
if ( !prop ) {
return '';
}
if ( typeof( prop ) == 'string' ) {
return prop;
}
if ( prop[ pageLang ] ) {
return prop[ pageLang ];
} else if ( prop.en ) {
return prop.en;
}
for ( var i in prop ) {
return prop[ i ];
}
return '';
}
function makeFile( text ) { // modern Browsers
const data = new Blob( [ text ], { type: 'application/gpx+xml' } );
if ( !gpxFile ) {
window.URL.revokeObjectURL( gpxFile );
}
gpxFile = window.URL.createObjectURL( data );
return gpxFile;
}
function getPhone( selector, $this ) {
var v = $( selector, $this ).first();
if ( v.length ) {
v = v.attr( dataPhone );
if ( v ) {
return v;
}
}
return '';
}
// Getting GeoJSON data sets from external sources (OSM, Commons)
function getGeoJSON( obj ) {
var promise, coordinates, geometry, i,
properties = obj.properties; // for all but not for 'page'
promise = $.getJSON( obj.url ).then( function( geoJSON ) {
switch ( obj.service ) {
case 'page': // from Commons
if ( geoJSON.jsondata && geoJSON.jsondata.data ) {
$.extend( obj, geoJSON.jsondata.data );
}
break;
case 'geoline': // from OSM
case 'geoshape':
$.extend( obj, geoJSON );
if ( properties ) {
for ( i = 0; i < obj.features.length; i++ ) {
if ( $.isEmptyObject( obj.features[ i ].properties ) ) {
obj.features[ i ].properties = properties;
} else {
obj.features[ i ].properties =
$.extend( {}, properties, obj.features[ i ].properties );
}
}
}
}
}, function() {
// failed, no tracks will be added
} );
return promise;
}
// getting Kartographer live data
function getKartographerLiveData() {
var i, obj, promiseArray = [];
const liveData = mw.config.get( 'wgKartographerLiveData' );
if ( liveData ) {
trackdata = liveData[ messages.track ];
if ( trackdata && !trackdata.length ) {
trackdata = null;
}
if ( trackdata ) {
for ( i = 0; i < trackdata.length; i++ ) {
obj = trackdata[ i ];
if ( obj.type === 'ExternalData' && obj.url ) {
promiseArray.push( getGeoJSON( obj ) );
}
}
}
}
// wait for getting all external data
if ( typeof Promise !== 'undefined' ) {
Promise.all( promiseArray )
.then( function() { createFile(); } )
.catch( function() { createFile(); } );
// create file also in case of failures
} else {
createFile();
}
return;
}
function writeTrack( coordinates, tracks, type, properties ) {
var j, k, s, coords;
if ( !coordinates || !coordinates.length ) {
return tracks;
}
tracks += '\n <trk>\n';
if ( properties ) {
s = replace( removeTags( getString( properties.title ) ) );
if ( s ) {
tracks += ' <name>' + s + '</name>\n';
}
s = replace( removeTags( getString( properties.desc ) ) );
if ( s ) {
tracks += ' <desc>' + s + '</desc>\n';
}
if ( properties.stroke ) {
tracks += ' <extensions>\n' +
' <gpxx:WaypointExtension xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">\n' +
' <gpxx:DisplayColor>' + properties.stroke + '</gpxx:DisplayColor>\n' +
' </gpxx:WaypointExtension>\n' +
' </extensions>\n';
}
}
for ( j = 0; j < coordinates.length; j++ ) {
coords = coordinates[ j ];
if ( type === 'MultiPolygon' ) {
coords = coordinates[ j ][ 0 ];
// only first polygon
// tests not yet completed
}
if ( coords.length ) {
tracks += ' <trkseg>\n';
for ( k = 0; k < coords.length; k++ ) {
tracks += ' <trkpt lat="' + coords[ k ][ 1 ]
+ '" lon="' + coords[ k ][ 0 ] + '" />\n';
}
tracks += ' </trkseg>\n';
}
}
return tracks + ' </trk>\n';
}
function writeTracks() {
var tracks = '';
if ( !trackdata || !trackdata.length ) {
return '';
}
var i, coordinates, geoJSON, geometry, properties;
for ( i = 0; i < trackdata.length; i++ ) {
if ( trackdata[ i ].features ) {
geoJSON = trackdata[ i ].features[ 0 ];
geometry = geoJSON.type === 'Feature' ? geoJSON.geometry : geoJSON;
coordinates = geometry ? geometry.coordinates : null;
properties = geoJSON.properties;
switch ( geometry.type ) {
case 'LineString':
tracks = writeTrack( [ coordinates ], tracks, 'MultiLineString', properties );
break;
case 'MultiLineString':
case 'Polygon':
tracks = writeTrack( coordinates, tracks, 'MultiLineString', properties );
break;
case 'MultiPolygon':
tracks = writeTrack( coordinates, tracks, 'MultiPolygon', properties );
}
}
}
return tracks;
}
function getText() { // generate GPX output
var markers = $( '.' + containerClass ).not( '.' + noGpxClass );
if ( !markers.length ) {
return '';
}
var aType, cmt, color, count, desc, gpxx, i, link, lat, lon, name,
text = '', v,
minlat = null, minlon = null, maxlat = null, maxlon = null; // for bounds
markers.each( function() {
var $this = $( this );
link = $( '.' + kartographerClass, $this ).first();
if ( link.length ) {
lat = link.attr( 'data-lat' );
lon = link.attr( 'data-lon' );
if ( minlat === null || lat < minlat ) {
minlat = lat;
}
if ( minlon === null || lon < minlon ) {
minlon = lon;
}
if ( maxlat === null || lat > maxlat ) {
maxlat = lat;
}
if ( maxlon === null || lon > maxlon ) {
maxlon = lon;
}
color = $this.attr( dataColor );
aType = $this.attr( dataType );
count = link.html();
if ( count.indexOf( '<' ) >= 0 ) {
count= ''; // no number but html tag
} else {
count = ' ' + ( '0' + count ).slice( -2 );
}
name = $this.attr( dataName );
if ( !name ) {
name = $( '.' + nameClass, $this ).first();
}
name = replace( removeTags( name ) );
desc = $( '.' + contentClass, $this ).first();
desc = ( desc.length ) ? replace( desc.text() ) : '';
cmt = '';
for ( i = 0; i < commentClasses.length; i++ ) {
v = $( '.' + commentClasses[ i ], $this ).first();
if ( v.length ) {
if ( cmt ) {
cmt += ' ';
}
cmt += replace( v.text() );
}
}
gpxx = '';
v = $( '.listing-address', $this ).first();
if ( v.length ) {
gpxx += ' <gpxx:Address>\n' +
' <gpxx:StreetAddress>' + replace( v.text() ) + '</gpxx:StreetAddress>\n' +
' </gpxx:Address>\n';
}
v = getPhone( '.listing-landline .listing-phone-number', $this );
if ( v ) {
gpxx += ' <gpxx:PhoneNumber Category="Phone">' + v + '</gpxx:PhoneNumber>\n';
}
v = getPhone( '.listing-tollfree .listing-phone-number', $this );
if ( v ) {
gpxx += ' <gpxx:PhoneNumber Category="Tollfree">' + v + '</gpxx:PhoneNumber>\n';
}
v = getPhone( '.listing-mobile .listing-phone-number', $this );
if ( v ) {
gpxx += ' <gpxx:PhoneNumber Category="Mobile">' + v + '</gpxx:PhoneNumber>\n';
}
v = getPhone( '.listing-fax .listing-phone-number', $this );
if ( v ) {
gpxx += ' <gpxx:PhoneNumber Category="Fax">' + v + '</gpxx:PhoneNumber>\n';
}
v = $( '.listing-email a', $this ).first();
if ( v.length ) {
gpxx += ' <gpxx:PhoneNumber Category="Email">' + v.text() + '</gpxx:PhoneNumber>\n';
}
v = $this.attr( dataUrl );
if ( v ) {
gpxx += ' <gpxx:PhoneNumber Category="URL">' + replace( v ) + '</gpxx:PhoneNumber>\n';
}
text += ' <wpt lat="' + lat + '" lon="' + lon + '">\n' +
' <name>[' + aType + count + '] ' + name + '</name>\n' +
' <type>' + aType + '</type>\n' +
' <extensions>\n' +
// ' <color>' + color + '</color>\n' +
' <gpxx:WaypointExtension xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">\n' +
' <gpxx:DisplayColor>' + color + '</gpxx:DisplayColor>\n' +
' <gpxx:DisplayMode>SymbolAndName</gpxx:DisplayMode>\n' +
gpxx +
' </gpxx:WaypointExtension>\n' +
' </extensions>\n';
if ( desc ) {
text += ' <desc>' + desc + '</desc>\n';
}
if ( cmt ) {
text += ' <cmt>' + cmt + '</cmt>\n';
}
text += ' </wpt>\n';
}
});
text += writeTracks();
if ( !text ) {
return '';
}
return '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n' +
'<gpx version="1.1" creator="Wikivoyage"\n' +
' xmlns="http://www.topografix.com/GPX/1/1"\n' +
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' +
' xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"\n' +
' xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\n' +
' http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www8.garmin.com/xmlschemas/GpxExtensions/v3/GpxExtensionsv3.xsd">\n\n' +
' <metadata>\n' +
' <name>' + replace( pageTitle ) + '</name>\n' +
' <desc>' + messages.gpxFileDescr + '</desc>\n' +
' <author>\n' +
' <name>Wikivoyage</name>\n' +
' </author>\n' +
' <copyright>\n' +
// ' <license>https://creativecommons.org/publicdomain/zero/1.0/</license>\n' +
' <license>https://creativecommons.org/licenses/by-sa/3.0/</license>\n' + // desc is CC-by-sa 3.0
' </copyright>\n' +
' <bounds minlat="'+ minlat + '" maxlat="' + maxlat + '" minlon="' + minlon +'" maxlon="' + maxlon + '"></bounds>\n' +
' </metadata>\n\n' +
text +
'</gpx>';
}
function createFile() {
var downloadText = getText();
if ( !downloadText ) {
return;
}
var fileName = pageTitle + '_' + pageLang + '.gpx';
fileName = fileName.replace( / |:|\/|\\/gi, '_' );
var image = $( '<img>', {
src: isMinerva ? imgSrcMinerva : imgSrc,
width: isMinerva ? '15' : '25',
height: isMinerva ? '20' : '25'
});
if ( isMinerva ) {
image.css( { 'margin-left': '3px' } ); // = ( 20 - 15 ) / 2
}
var indicator = $( '<a>', {
id: 'mw-indicator-i3-gpx',
class: 'mw-indicator',
title: messages.gpxTitle
} )
.css( { display: 'inline-block' } )
.append( image )
.attr( {
href: makeFile( downloadText ),
download: fileName
} );
if ( isMinerva ) { // mobile view
indicator.append( document.createTextNode( ' ' + messages.gpxLabel ) );
indicator = $( '<li></li>', {
id: 'page-actions-poi2gpx',
class: 'page-actions-menu__list-item'
} )
.append( indicator );
$( minervaPageActionsSelector ).after( indicator );
} else {
var indicators = $( '.mw-indicators' ).first(); // always on desktop
var geoIndicator;
// international version: only:
// indicators.prepend( indicator );
indicators.each( function() {
geoIndicator = $( '#mw-indicator-i3-geo', $( this ) );
if ( geoIndicator.length ) {
geoIndicator.after( indicator );
} else {
$( this ).prepend( indicator );
}
});
}
}
function allowedForCurrentPage() {
const namespace = mw.config.get( 'wgNamespaceNumber' );
return allowedNamespaces.includes( namespace ) &&
mw.config.get( 'wgAction' ) == 'view';
}
function init() {
if ( !allowedForCurrentPage() ) {
return;
}
if ( typeof Blob === 'undefined' ) { // very old browsers
return;
}
setupMessages();
getKartographerLiveData();
// calls createFile
}
return { init: init };
} ();
$( poi2gpx.init );
} ( jQuery, mediaWiki ) );
//</nowiki>