MediaWiki:MapTools.js

Aus Wikivoyage
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Speichern 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: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Gehe zu Menü → Einstellungen (Opera → Einstellungen auf dem Mac) und dann auf Datenschutz & Sicherheit → Browserdaten löschen → Gespeicherte Bilder und Dateien.
//<nowiki>
/***************************************************************************
 * mapTools v1.8, 2018-11-15
 * Several map creation and supporting tools
 * Original author: Roland Unger
 * Support of desktop and mobile views
 * Documentation: https://de.wikivoyage.org/wiki/Wikivoyage:mapTools.js
 * License: GPL-2.0+, CC-by-sa 3.0
 ***************************************************************************/

( function( $, mw ) {
	'use strict';

	var mapTools = function() {
		var indicatorClass = '.wv-coord-indicator';
		var imgSrc = 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/WMA_button2b.png/24px-WMA_button2b.png';
		var mapContainerId = 'wv-topMap';
		var maxZoomLevel = 19;
		var defaultZoomLevel = 17;
		var defaultMapZoomLevel = 14;
		var defaultNearbyZoomLevel = 10;

		var nearbyClass = '.wv-open-nearby-map';
		var fullScreenContainerId = 'wv-fullScreenMap';

		var articlesMapId = 'wv-articles-map';
		var articlesMapTitle = 'Übersicht der Wikivoyage-Artikel';

		var mapframeContainer = '.mw-kartographer-container';
		var mapframeMap = '.mw-kartographer-map';

		var containerClass = '.vcard'; // wrapper class of a single marker or listing
		var kartographerClass = '.mw-kartographer-maplink';
		var nameClass = 'listing-name';
		var imageClass = 'listing-image';
		var dataLat = 'data-lat';
		var dataLon = 'data-lon';
		var dataZoom = 'data-zoom';
		var dataName = 'data-name';
		var dataColor = 'data-color';
		var dataSymbol = 'data-symbol';
		var dataNumber = 'data-number';
		var dataGroup = 'data-group-translated'; // other wikis: 'data-type'
		var dataDialog = 'data-dialog';
		var dataHeight = 'data-height';
		var dataOverlays = 'data-overlays';
		
		var defaultShow = '["Maske","Track","Aktivitaet","Anderes","Anreise","Ausgehen","Aussicht","Besiedelt","Fehler","Gebiet","Kaufen","Kueche","Sehenswert","Unterkunft","aquamarinblau","blau","blaugruen","braun","cosmos","gold","grau","hellgruen","koenigsblau","magentarot","marineblau","orange","pflaumenblau","purpurrot","rot","rotbraun","schokobraun","schwarz","silber","stahlblau","violett","waldgruen"]';
		var defaultShowArray = JSON.parse( defaultShow );

		var titleIndicator = 'Klick öffnet und schließt die Karte für ';
		var titleNearby = 'Klick öffnet die Umgebungskarte für ';
		var titleClose = 'Schließen';
		var mapOf = 'Karte von ';
		var nearbyMapOf = 'Umgebungskarte von ';
		var mapCenter = 'Kartenzentrum';
		var defaultGroup = 'Karte';
		var mask = 'Maske'; // mask layer name
		var track = 'Track'; // track layer name
		var magnifyMap = 'Karte vergrößern';

		var data = {};
		var groups = [];

		// creating a Kartographer map
		var createMap = function( id, center, zoom, caption, options ) {
			function contains( arr, obj ) {
				for ( var i = 0; i < arr.length; i++ ) {
        			if (arr[ i ] === obj) return true;
    			}
    			return false;
			}

			mw.loader.using( [ 'ext.kartographer.box' ] ).then( function() {
				var $id = $( id );
				var $body = $( 'body' );
				var layerOptions;

				// for simple full-screen map
				if ( !options.withDialog && options.isFullScreen ) {
					$body.css( { overflow: 'hidden' } );
					$id.css( { position: 'fixed', height: '100%', width: '100%',
						top: 0, left: 0, 'z-index': 101 } ); // vector skin
				}			

				// creating base map

				// fortunately ext.kartographer.box is not validating the
				// GeoJSON against the GeoJSON+simplestyle schema
				// as it is done by maplink/mapframe tags
				// https://github.com/mapbox/simplestyle-spec/tree/master/1.1.0
				// see also: phabricator task T181604

				var kartoBox = mw.loader.require( 'ext.kartographer.box' );
				var map = kartoBox.map( {
					container: $id[ 0 ],
					center: center,
					zoom: zoom,
					allowFullScreen: options.allowFullScreen,
					alwaysInteractive: true,
					captionText: caption,
					fullscreen: options.isFullScreen,
					featureType: options.featureType
				} );
				// following line is necessary for proper loading of
				// map-dialog sidebar
				map.initView( center, zoom );
				// the following property is used by Kartographer.js
				if ( options.toggleNearby ) map.toggleNearby = true;

				// adding markers by group names
				if ( options.withData )
					$.each( groups, function( i, group ) {
						if ( contains( options.show, group.id ) ) {
							layerOptions = {};
							if ( group.name !== '' ) {
								layerOptions.name = group.name;
								layerOptions.attribution = group.name;
							}
							else
								if ( group.attribution && group.attribution !== '' )
									layerOptions.attribution = group.attribution;
							map.addGeoJSONLayer( group.id, data[ group.id ], layerOptions );
						}
					} );

				// adding dialog to full-screen map
				if ( options.withDialog ) {
					$id.addClass( 'mw-kartographer-mapDialog-map' );
					mw.loader.using( 'ext.kartographer.dialog' ).done( function() {
						map.doWhenReady( function() {
							mw.loader.require( 'ext.kartographer.dialog' ).render( map );
						} );
					} );
				}
				else {
					// adding Close control
					if ( options.withClose ) {
						var controls = $( '.leaflet-top.leaflet-right', $id );
						var control = $( '<div class="leaflet-bar leaflet-control-static leaflet-control"></div>' )
							.append( $( '<a class="closeControl"></a>' )
								.attr( { title: titleClose, role: 'button',
									'aria-disabled': 'false' } )
								.click( function() {
									$id.remove();
									if ( options.isFullScreen )
										$body.css( { overflow: 'auto' } );
								} )
							);
						controls.prepend( control );
					}

					if ( options.isFullScreen ) {
						$( document ).keydown( function( event ) {
							if ( event.keyCode === 27 ) { // ESC
								event.preventDefault();
								$id.remove();
								$body.css( { overflow: 'auto' } );
							}
						} );
					}

					// adding Nearby and Layers controls using Kartographer.js
					if ( options.withControls ) mw.hook( 'wikipage.maps' ).fire( map );
				}
			} );
		};

		// creating GeoJSON data separated by group
		var singleDataset = function( color, symbol, title, lat, lon,
			description, group ) {
			if ( group === null || group === '' ) group = defaultGroup;
			if ( !data.hasOwnProperty( group ) ) data[ group ] = [];
		
			data[ group ].push( {
				'type': 'Feature',
				properties: {
					'marker-color': color,
					'marker-size': 'medium',
					'marker-symbol': symbol,
					title: title,
					description: description
				},
				geometry: {
					'type': 'Point',
					coordinates: [ lon, lat ]
				}
			} );
		};

		// Getting GeoJSON data sets from external sources (OSM, Commons)
		var getGeoJSON = function( obj ) {
			var promise, coordinates, geometry, i, j;
			var world = [ [ [ 3600, -180 ], [ 3600, 180 ], [ -3600, 180 ], [ -3600, -180 ], [ 3600, -180 ] ] ];
			var properties = obj.properties; // for all but not for 'page'

			promise = $.getJSON( obj.url ).then( function( geoJSON ) {
				switch ( obj.service ) {
					case 'page':
						if ( geoJSON.jsondata && geoJSON.jsondata.data )
							$.extend( obj, geoJSON.jsondata.data );
						break;

					case 'geomask':
						coordinates = world;
						for ( i = 0; i < geoJSON.features.length; i++ ) {
							geometry = geoJSON.features[ i ].geometry;
							if ( !geometry ) continue;

							// push only first polygon
							switch ( geometry.type ) {
								case 'Polygon':
									coordinates.push( geometry.coordinates[ 0 ] );
									break;
								case 'MultiPolygon':
									for ( j = 0; j < geometry.coordinates.length; j++ )
										coordinates.push( geometry.coordinates[ j ][ 0 ] );
							}
						}
						obj.type = 'Feature';
						obj.geometry = { type: 'Polygon', coordinates: coordinates };
						if ( !properties )
							properties = { 'stroke-width': 2, 'fill-opacity': 0.5 };
						if ( $.isEmptyObject( obj.properties ) )
							obj.properties = properties;
						else
							obj.properties = $.extend( {}, properties, obj.properties );
						break;

					case 'geoline':
					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
			} );

			return promise;
		};

		// Creating attribution strings
		var getAttribution = function( obj ) {
			var uri = new mw.Uri( obj.url );
			var link = '';

			switch ( obj.service ) {
				case 'page':
					link = mw.msg( 'project-localized-name-commonswiki' ) + ': '
						+ '<a target="_blank" href="'
						+ '//commons.wikimedia.org/wiki/Data:' + encodeURI( uri.query.title )
						+ '">' + uri.query.title + '</a>';
					break;

				default: // other services
			}
			
			return link;
		};

		// getting Kartographer live data
		var getKartographerLiveData = function() {
			var group, i, obj;
			var promiseArray = [];
			var attributions, link;

			data = mw.config.get( 'wgKartographerLiveData' );
			if ( data !== null ) {
				groups = [];
				for ( group in data )
					// ignoring empty groups
					if ( data[ group ].length > 0 ) {
						attributions = [];
						for (i = 0; i < data[ group ].length; i++) {
							obj = data[ group ][i];
							if ( obj.type === 'ExternalData' && obj.url ) {
								promiseArray.push( getGeoJSON( obj ) );
								link = getAttribution( obj );
								if ( link !== '' ) attributions.push( link );
							}
						}
						attributions = attributions.join( ', ' );
						if ( group === mask ) groups.unshift( { id: group, name: '' } );
						else groups.push( { id: group, name: '', attribution: attributions } );
					}
			}

			groups.sort( function( a, b ) {
				// mask has to be the first layer
				if ( a.id === mask ) return -1;
				if ( b.id === mask ) return 1;
				if ( a.id === track && b.id === mask ) return 1;
				if ( b.id === track && a.id === mask ) return -1;
				if ( a.id === track && b.id !== mask ) return -1;
				if ( b.id === track && a.id !== mask ) return 1;
				return a.id.localeCompare( b.id );
			} );

			// wait for getting all external data
			var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
			if ( isIE11 )
				$.when.apply( $, promiseArray ).then( function() {
					initMapTools();
				} );
			else
				Promise.all( promiseArray ).then( function() {
					initMapTools();
				} );
			return;
		};

		// getting all vCard/listing and marker information from article
		var getData = function() {
			var group;

			// initally try to get wgKartographerLiveData because of masks
			// no marker(s): mw.config.get( 'wgKartographerLiveData' ) returns null
			// no map(s): all group arrays like see, do, etc. are empty
			// see phabricator task T183770

			// getData is called by getKartographerLiveData() therefore groups
			// object is set
			if ( groups.length > 0 ) return;

			// no wgKartographerLiveData or empty arrays
			data = {};
			var markers = $( containerClass );
			if ( markers.length === 0 ) return;

			var lat, lon, title, symbol, color, desc, image, number;
			var link, wikiLink, $this;

			markers.each( function() {
				$this = $( this );
				link = $( kartographerClass, $this ).first();
				if ( link.length > 0 ) {
					lat = link.attr( dataLat );
					lon = link.attr( dataLon );
					color = $this.attr( dataColor );
					group = $this.attr( dataGroup );

					// check if only marker number and no HTML tag
					number = link.html();
					if ( number.indexOf( '<' ) < 0 ) symbol = number;
					else symbol = $this.attr( dataSymbol );

					// getting title
					title = $( '.' + nameClass, $this ).first();
					wikiLink = $( 'a', title ).first();
					if ( wikiLink.length > 0 ) title = title.html();
					else title = $this.attr( dataName );

					// putting image to description
					desc = '';
					image = $( '.' + imageClass, $this );
					if ( image.length > 0 )
						desc = image.html()
							// for mobile view: show image from noscript instead of placeholder
							.replace( '<noscript>', '' ).replace( '</noscript>', '' );

					// adding to GeoJSON data table
					singleDataset( color, symbol, title, lat, lon, desc, group );
				}
			} );

			// creating sorted group list
			groups = [];
			for ( group in data ) groups.push( { id: group, name: '' } );
			groups.sort( function( a, b ) {
				// mask has to be the first layer
				if ( a.id === mask ) return -1;
				if ( b.id === mask ) return 1;
				return a.id.localeCompare( b.id );
			} );
		};

		// displaying a map by clicking the geo-indicator button
		var indicatorMap = function() {
			var indicator = $( indicatorClass ).first();
			if ( indicator.length === 0) return;

			var id = mapContainerId;
			var options = {
				withClose: true,
				withControls: true,
				withData: true,
				show: defaultShowArray,
				withDialog: false,
				toggleNearby: false,
				allowFullScreen: true,
				isFullScreen: false,
				featureType: 'mapframe'
			};

			var zoom = indicator.attr( dataZoom );
			if ( zoom === undefined || zoom < 0 || zoom > maxZoomLevel )
				zoom = defaultMapZoomLevel;
			var lat = indicator.attr( dataLat );
			var lon = indicator.attr( dataLon );
			var center = [ lat, lon ];			

			// no POIs --> show blue map-center marker
			if ( groups.length === 0 ) {
				singleDataset( '#3366cc', '', mapCenter, lat, lon, '', defaultGroup );
				groups = [ { id: defaultGroup, name: '' } ];
			}

			var indicatorImg, indicatorDiv;
			if ( mw.config.get( 'skin' ) === 'minerva' ) { // mobile view
				indicatorImg = $( '<img>' )
					.attr( 'src', imgSrc )
					.attr( 'title', titleIndicator + mw.config.get( 'wgTitle' ) )
					.css( { cursor: 'pointer' } )
					.click( function() { 
						var container = $( '#' + id );
						if ( container.length === 0 ) {
							$( '#bodyContent' ).prepend( $( '<div></div>' )
								.attr( { 'id': id, role: 'dialog' } )
								.css( 'margin-top', '0.7em' )
							);
							createMap( '#' + id, center, zoom,
								mapOf + mw.config.get( 'wgTitle' ), options );
						}
						else container.remove();
				} );
				indicatorDiv = $( '<div id="mw-indicator-i3-gpx" class="mw-indicator">' )
					.append( indicatorImg )
					.css( 'float', 'right' );
				$( '#section_0' ).after( indicatorDiv );
			}
			else { // desktop views
				// replacing indicator image and adding event handlers

				$( indicatorClass + ' .map-globe-default' )
					.css( { display: 'none' } );
				$( indicatorClass + ' .map-globe-js' )
					.css( { display: 'inline', cursor: 'pointer' } )
					.attr( 'title', titleIndicator + mw.config.get( 'wgTitle' ) )
					.click( function() {
						var container = $( '#' + id );
						if ( container.length === 0 ) {
							$( '#contentSub' ).after( $( '<div></div>' )
								.attr( { 'id': id, role: 'dialog' } )
							);
							createMap( '#' + id, center, zoom,
								mapOf + mw.config.get( 'wgTitle' ), options );
						}
						else container.remove();
				} );
			}
		};

		// displaying nearby maps by clicking a nearby link
		var nearbyMap = function() {
			var nearbyLinks = $( nearbyClass );
			if ( nearbyLinks.length === 0) return;

			var id = fullScreenContainerId;
			var options = {
				withClose: true,
				withControls: true,
				withData: true,
				show: defaultShowArray,
				withDialog: true,
				toggleNearby: true,
				allowFullScreen: false,
				isFullScreen: true,
				featureType: 'maplink'
			};

			var $this, link, parent;
			var center, dialog, lat, lon, zoom;

			nearbyLinks.each( function() {
				$this = $( this );
				$this.parent().show();

				// link will replace span tag: copying data
				link = $( '<a></a>' )
					.html( $this.html() )
					.css( { cursor: 'pointer' } )
					.attr( 'title', titleNearby + mw.config.get( 'wgTitle' ) )
					.attr( dataLat, $this.attr( dataLat ) )
					.attr( dataLon, $this.attr( dataLon ) );
				zoom = $this.attr( dataZoom );
				if ( zoom !== undefined ) link.attr( dataZoom, zoom );
				dialog = $this.attr( dataDialog );
				if ( dialog !== undefined ) link.attr( dataDialog, dialog );

				link.click( function( event ) {
					var container = $( '#' + id );
					if ( container.length === 0 ) {
						var target = $( event.target );

						lat = target.attr( dataLat );
						if ( lat === undefined ) lat = 0;
						lon = target.attr( dataLon );
						if ( lon === undefined ) lon = 0;
						center = [ lat, lon ];
						zoom = target.attr( dataZoom );
						if ( zoom === undefined || zoom < 0 || zoom > maxZoomLevel )
							zoom = defaultNearbyZoomLevel;
						dialog = target.attr( dataDialog );
						if ( dialog === undefined ) dialog = 'false';
						options.withDialog = ( dialog === 'true' || dialog === 'yes' );

						$( 'body' ).append( $( '<div></div>' )
							.attr( { 'id': id, role: 'dialog' } )
						);

						createMap( '#' + id, center, zoom,
							nearbyMapOf + mw.config.get( 'wgTitle' ), options );
					}
					else container.remove();
				} );

				$this.replaceWith( link );
			} );
		};

		// replacing the Maplink links by MapTools to show Wikivoyage controls
		// see also: phabricator T180909
		var replaceMaplinks = function() {
			var links = $( kartographerClass );
			if ( links.length === 0 ) return;

			var id = fullScreenContainerId;
			var options = {
				withClose: true,
				withControls: true,
				withData: true,
				show: null,
				withDialog: true,
				toggleNearby: false,
				allowFullScreen: false,
				isFullScreen: true,
				featureType: 'maplink'
			};

			var center, lat, lon, name, number, target, wrapper, zoom, $this;

			links.each( function() {
				$this = $( this );

				$this.removeAttr( 'href' )
					.css( { cursor: 'pointer', 'pointer-events': 'auto',
						'text-decoration': 'none' } );

				$this.click( function( event ) {
					// marker could be contain an image -> closest
					target = $( event.target ).closest( kartographerClass );
					wrapper = target.closest( containerClass );

					lat = target.attr( dataLat );
					lon = target.attr( dataLon );
					center = [ lat, lon ];
					zoom = target.attr( dataZoom );
					if ( zoom === undefined || zoom < 0 || zoom > maxZoomLevel )
						zoom = defaultZoomLevel;

					name = '';
					number = target.html();
					if ( number.indexOf( '<' ) >= 0 ) { 
						number = '';
						name = target.text();
					}
					else if ( number === '' || isNaN( number ) ) {
						name = number;
						number = '';
					}
					if ( name === '' && wrapper.length > 0 )
						name = wrapper.attr( dataName );
					if ( name !== '' && number !== '' ) number = number + ': ';

					options.show = target.attr( dataOverlays );
					if ( options.show === undefined )
						options.show = defaultShowArray;
					else options.show = JSON.parse( options.show );

					$( 'body' ).append( $( '<div></div>' )
						.attr( { 'id': id, role: 'dialog' } ) );
					createMap( '#' + id, center, zoom, number + name, options );
				} );
			} );
		};

		// showing all articles on an earth map
		var articlesMap = function() {
			var map = $( '#' + articlesMapId ).first();
			if ( map.length === 0 ) return;

			var options = {
				withClose: false,
				withControls: true,
				withData: false,
				withDialog: false,
				toggleNearby: true,
				allowFullScreen: false,
				// because Nearby mode cannot be toggled in full-screen mode
				isFullScreen: false,
				featureType: 'mapframe'
			};

			var zoom = Math.floor( map.height() / 500 );
			if ( zoom < 0 ) zoom = 0;
			if ( zoom > maxZoomLevel ) zoom = maxZoomLevel;
			createMap( '#' + articlesMapId, [ 0, 0 ], zoom, articlesMapTitle, options );
		};

		// adding a magnify button to Kartographer container
		var addMagnifyButton = function() {
			var maps = $( mapframeContainer );
			if ( maps.length === 0 ) return;

			var id = fullScreenContainerId;
			var options = {
				withClose: true,
				withControls: true,
				withData: true,
				show: null,
				withDialog: true,
				toggleNearby: false,
				allowFullScreen: false,
				isFullScreen: true,
				featureType: 'maplink'
			};

			var caption, center, height, link, map, name, target, zoom,
				zoomIncr, $this;

			maps.each( function() {
				$this = $( this );

				// no magnify button if zoom is already maxZoomLevel
				// not in frameless mode
				map = $( mapframeMap, $this ).first();
				caption = $( '.thumbcaption', $this ).first();
				zoom = Number( map.attr( dataZoom ) );
				if ( isNaN( zoom ) ) zoom = defaultMapZoomLevel;
				if ( map.length > 0 && caption.length > 0 && zoom < maxZoomLevel) {
					link = $( '<a class="internal"></a>' )
						.css( { cursor: 'pointer' } )
						.attr( 'title', magnifyMap )
						.click( function( event ) {
							target = $( event.target );
							map = target.closest( mapframeContainer );
							caption = $( '.thumbcaption', map ).first();
							name = caption.text();

							// getting initial position from data if lat or lon
							// or zoom are undefined
							map = $( mapframeMap, map ).first();
							center = [ map.attr( dataLat ), map.attr( dataLon ) ];
							zoom = Number( map.attr( dataZoom ) );
							if ( isNaN( zoom ) ) zoom = undefined;
							else {
								zoomIncr = 1;
								height = screen.height / map.attr( dataHeight );
								if ( height > 4 ) zoomIncr++;
								if ( height > 8 ) zoomIncr++;
								zoom += zoomIncr;
								if ( zoom > maxZoomLevel ) zoom = maxZoomLevel;
							}

							options.show = map.attr( dataOverlays );
							if ( options.show === undefined )
								options.show = defaultShowArray;
							else options.show = JSON.parse( options.show );

							$( 'body' ).append( $( '<div></div>' )
								.attr( { 'id': id, role: 'dialog' } ) );
							createMap( '#' + id, center, zoom, name, options );
						} );
					caption.prepend( $( '<div class="magnify"></div>' ).append( link ) );
				}
			} );
		};

		// calling all functions
		var initMapTools = function() {
			getData(); // getting POIs from article
			indicatorMap();
			nearbyMap();
			replaceMaplinks();
			articlesMap();
			addMagnifyButton();
		};

		var init = function() {
			getKartographerLiveData();
			// calls initMapTools
		};

		return { init: init };
	} ();

	$( mapTools.init );

} ( jQuery, mediaWiki ) );

//</nowiki>