Замечание: Возможно, после публикации вам придётся очистить кэш своего браузера, чтобы увидеть изменения.

  • Firefox / Safari: Удерживая клавишу Shift, нажмите на панели инструментов Обновить либо нажмите Ctrl+F5 или Ctrl+R (⌘+R на Mac)
  • Google Chrome: Нажмите Ctrl+Shift+R (⌘+Shift+R на Mac)
  • Internet Explorer / Edge: Удерживая Ctrl, нажмите Обновить либо нажмите Ctrl+F5
  • Opera: Нажмите Ctrl+F5.
//<nowiki>
/*********************************************************************
 * poi2gpx v1.4, 2022-06-02
 * Download of article’s points of interest and tracks to a GPX file
 * Original author: Roland Unger
 * adaptation: Bok
 * 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
 *********************************************************************/

( function ( $, mw ) {
	'use strict';
	
	var poi2gpx = function() {
		var imgSrc = 'https://upload.wikimedia.org/wikivoyage/de/thumb/f/f0/WV-poi2gpx.svg/50px-WV-poi2gpx.svg.png';
		var imgSrcMinerva = 'https://upload.wikimedia.org/wikivoyage/de/thumb/6/63/WV-poi2gpx-black.svg/50px-WV-poi2gpx-black.svg.png';
			// image for download link
		var download = 'Download der Kartenpositionen als GPX-Datei'; // tooltip
		var mainDesc = 'Kartenpositionen aus dem deutschen Wikivoyage-Artikel'; // GPX description
		var gpxLabel = 'GPX';

		var containerClass = 'vcard'; // contains wrapper markup of a single marker or listing
		var noGpxClass = 'listing-no-gpx';
		var kartographerClass = 'mw-kartographer-maplink';
		var nameClass = 'listing-name';
		var contentClass = 'listing-content';
		var dataName = 'data-name';
		var dataColor = 'data-color';
		var dataType = 'data-group'; // other wikis: 'data-type'
		var dataUrl = 'data-url';
		var dataPhone = 'data-phone';
		var comments = [ 'listing-hours', 'listing-checkin', 'listing-checkout', 'listing-price', 'listing-credit' ];

		var gpxFile = null; // check for URL object
		var trackdata = null;

		var translations = {
			area:      'Gebiet',
			buy:       'Покупки',
			'do':      'Чем заняться',
			drink:     'Ночная жизнь',
			eat:       'Еда',
			error:     'Неизвестно',
			go:        'Как добраться',
			other:     'Разное',
			populated: 'Besiedelt',
			see:       'Достопримечательности',
			sleep:     'Где остановиться',
			track:     'Track', // group name of tracks, intl. version: 'track'
			view:      'Aussicht',

			cosmos:    'Cosmos',
			gold:      'Gold',
			lime:      'Hellgrün',
			mediumaquamarine: 'Aquamarinblau',
			orange:    'Orange',
			plum:      'Pflaumenblau',
			purple:    'Violett',
			red:       'Rot',
			silver:    'Silber'
		};

		var makeFile = function( text ) { // modern Browsers
	    	var data = new Blob( [text], { type: 'application/gpx+xml' } );
	    	if ( gpxFile !== null ) window.URL.revokeObjectURL( gpxFile );
	    	gpxFile = window.URL.createObjectURL( data );
	    	return gpxFile;
		};

		var ieSaveFile = function( text, fileName ) { // IE 11
			var data = new Blob( [text], { type: 'application/gpx+xml' } );
			window.navigator.msSaveOrOpenBlob( data, fileName );
		};

		var replace = function( text ) { // to use text in XML tags
			return text.replace( /\&/g, '&amp;' )
				.replace( /"/g, '&quot;' )
				.replace( /</g, '&lt;' )
				.replace( />/g, '&gt;' );
		};

		var getPhone = function( selector, $this ) {
			var r = '';
			var v = $( selector, $this ).first();
			if ( v.length !== 0 ) {
				v = v.attr( dataPhone );
				if ( v !== undefined ) r = v;
			}
			return r;
		};

		// Getting GeoJSON data sets from external sources (OSM, Commons)
		var getGeoJSON = function( obj ) {
			var promise, coordinates, geometry, i;
			var 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
		var getKartographerLiveData = function() {
			var i, obj;
			var promiseArray = [];

			trackdata = mw.config.get( 'wgKartographerLiveData' );
			if ( trackdata ) {
				trackdata = trackdata[ translations.track ];
				if ( trackdata && trackdata.length === 0 )
					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
			var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
			if ( isIE11 )
				$.when.apply( $, promiseArray ).then( function() {
					createFile();
				} );
			else
				if ( typeof Promise !== 'undefined' )
					Promise.all( promiseArray )
						.then( function() { createFile(); } )
						.catch( function() { createFile(); } );
						// create file also in case of failures
				else
					createFile();
			return;
		};

		var getString = function( prop ) {
			if ( !prop ) return '';
			if ( typeof( prop ) == 'string' ) return prop;
			var wikiLang = mw.config.get( 'wgPageContentLanguage' );
			if ( prop[ wikiLang ] ) return prop[ wikiLang ];
			if ( prop.en ) return prop.en;
			for ( var i in prop ) { return prop[ i ] }
			return '';
		};

		var removeTags = function( s ) {
			return $( '<div>' + s + '</div>' ).text();
		};

		var writeTrack = function( coordinates, tracks, type, properties ) {
			var j, k, s, coords;
			if ( !coordinates || coordinates.length === 0 ) 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 > 0 ) {
					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';
		};

		var writeTracks = function() {
			var tracks = '';
			if ( !trackdata || trackdata.length === 0 ) return '';
			
			var i, geoJSON, geometry, coordinates, 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':
							tracks = writeTrack( coordinates, tracks, 'MultiLineString', properties );
							break;

						case 'Polygon':
							tracks = writeTrack( coordinates, tracks, 'MultiLineString', properties );
							break;
						case 'MultiPolygon':
							tracks = writeTrack( coordinates, tracks, 'MultiPolygon', properties );
					}
				}
			}
			
			return tracks;
		};

		var getText = function() { // generate GPX output
			var markers = $( '.' + containerClass ).not( '.' + noGpxClass );
			if ( markers.length === 0 ) return '';

			var link, lat, lon, count, i, name, aType, translType, color,
				desc, cmt, v, gpxx;
			var text = '';
			var minlat = null, minlon = null, maxlat = null, maxlon = null; // for bounds

			markers.each( function() {
				var $this = $( this );
				link = $( '.' + kartographerClass, $this ).first();
			
				if ( link.length > 0 ) {
					lat = link.attr( 'data-lat' );
					lon = link.attr( 'data-lon' );
					if ( minlat === null ) minlat = lat;
					if ( minlon === null ) minlon = lon;
					if ( maxlat === null ) maxlat = lat;
					if ( maxlon === null ) maxlon = lon;
					if ( lat < minlat ) minlat = lat;
					if ( lat > maxlat ) maxlat = lat;
					if ( lon < minlon ) minlon = lon;
					if ( lon > maxlon ) maxlon = lon;

					color = $this.attr( dataColor );
					if ( color === undefined ) {
						color = link.attr("style").split(";")[0].split(":")[1].trim();
					}
					aType = $this.attr( dataType );
					translType = aType;
					if ( aType in translations && translations[aType] !== '' )
						translType = translations[aType];

					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 === undefined ) {
						name = $( '.' + nameClass, $this ).first().text(); // исправлено в русской версии
					}
					name = replace( removeTags( name ) );

					desc = $( '.' + contentClass, $this ).first();
					if ( desc.length === 0 ) desc = '';
					else desc = replace( desc.text() );

					cmt = '';
					for ( i = 0; i < comments.length; i++ ) {
						v = $( '.' + comments[ i ], $this ).first();
						if ( v.length !== 0 ) {
							if ( cmt !== '' ) cmt += ' ';
							cmt += replace( v.text() );
						}
					}

					gpxx = '';
					v = $( '.listing-address', $this ).first();
					if ( v.length !== 0 ) {
						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 !== 0 ) {
						gpxx += '        <gpxx:PhoneNumber Category="Email">' + v.text() + '</gpxx:PhoneNumber>\n';
					}
					v = $this.attr( dataUrl );
					if ( v !== undefined ) {
						gpxx += '        <gpxx:PhoneNumber Category="URL">' + replace( v ) + '</gpxx:PhoneNumber>\n';
					}

					text += '  <wpt lat="' + lat + '" lon="' + lon + '">\n' +
						'    <name>[' + translType + 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';
				}
			});

			var tracks = writeTracks();
			text += tracks;

			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( mw.config.get( 'wgTitle' ) ) + '</name>\n' +
				'    <desc>' + mainDesc + '</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>';
		};

		var allowedForCurrentPage = function() {
			var namespace = mw.config.get( 'wgNamespaceNumber' );
			if (namespace !== 0 && namespace !== 2 && namespace !== 4) {
				return false;
			}
			return mw.config.get('wgAction') == 'view';
		};

		var createFile = function() {
			if ( typeof Blob === "undefined" ) return; // very old browsers
			if ( !allowedForCurrentPage() ) return;

			var downloadText = getText();
			if ( downloadText === '' ) return;
		
			var isMinerva = mw.config.get( 'skin' ) === 'minerva';  // mobile view
			var fileName = mw.config.get( 'wgTitle' ) + '_'
				+ mw.config.get( 'wgPageContentLanguage' ) + '.gpx';
			fileName = fileName.replace( / |:|\/|\\/gi, '_' );

			var image = $( '<img>', {
					src: isMinerva ? imgSrcMinerva : imgSrc,
					width: isMinerva ? '15' : '25',
					height: isMinerva ? '20' : '25',
	    			alt: download,
					title: download
				});
			if ( isMinerva ) {
				image.css( { 'margin-left': '3px' } ); // = (20-15)/2
			}
			var indicator = $( '<a>', {
					id: 'mw-indicator-i3-gpx',
					class: 'mw-indicator',
					title: download
				} )
				.css( { display: 'inline-block' } )
				.append( image );
			if ( !window.navigator.msSaveOrOpenBlob ) { // modern browsers
				indicator.attr( {
					href: makeFile( downloadText ),
		    		download: fileName
		    	} );
			} else { // ie 11
				indicator.click( function() {
					ieSaveFile( downloadText, fileName );
				} );
			}

			if ( isMinerva ) { // mobile view
				indicator.append( document.createTextNode( ' ' + 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 === 0 ) {
						$(this).prepend( indicator );
					}
					else {
						geoIndicator.after( indicator );
					}
				});
			}
		};
	
		var init = function() {
			getKartographerLiveData();
			// calls createFile
		};

		return { init: init };
	} ();
	
	$( poi2gpx.init );

} ( jQuery, mediaWiki ) );

//</nowiki>