MediaWiki:Gadget-Poi2gpx.js

Aus Wikivoyage

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
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
//<nowiki>
/*********************************************************************
 * poi2gpx v1.5, 2023-12-10
 * 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 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, '&amp;' )
				.replace( /"/g, '&quot;' )
				.replace( /</g, '&lt;' )
				.replace( />/g, '&gt;' );
		}

		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 );
				$( '#page-actions #page-actions-edit' ).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>