MediaWiki:Gadget-Maptool.js

/* A tool based on the original code as stated below, that helps searching for POIs and creating dynamic maps. // /***************************************************************************** * mapTools v2.0, 2023-06-16 * Several map creation and supporting tools * Original author: Roland Unger * Support of desktop and mobile views * Documentation: https://de.wikivoyage.org/wiki/Wikivoyage:Gadget-MapTools.js * License: GPL-2.0+, CC-by-sa 3.0 ****************************************************************************/ /* eslint-disable mediawiki/class-doc */

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

var mapTools = function {

// https://www.mediawiki.org/wiki/Help:Extension:Kartographer/Icons const wdMakiMap = { "Q39816": "mountain", "Q8502": "mountain", "Q46831": "mountain", "Q43501": "zoo", "Q515": "city", "Q532": "village", "Q3957": "town", "Q2983893": "city", // quarter "Q40080": "beach", "Q44782": "harbor", "Q8072": "volcano", "Q3848936": "park", // protected area "Q473972": "park", // protected landscape "Q46169": "park", // national park "Q22746": "park", // urban park "Q23397": "water", // lake "Q35509": "tunnel", "Q34038": "waterfall", "Q16970": "religious-christian", "Q33506": "museum", "Q5487333": "beer", // microbrewery "Q131734": "beer", // brewery "Q4989906": "monument", "Q179700": "monument", // statue "Q6017969": "viewpoint", "Q12323": "dam", "Q751876": "castle", "Q483110": "stadium", "Q1007870": "art-gallery", "Q2281788": "aquarium", "Q45782": "aquarium", "Q4421": "natural", // forest "Q4022": "water", // river "Q55490": "rail", // through station "Q55488": "rail", // railway station "Q1268865": "rail", // light rail "Q3914": "school", "Q23413": "castle", "Q11166728": "communications-tower", "Q1798641": "communications-tower", "Q22698": "park", "Q24354": "theatre", "Q1060829": "theatre", // concert hall "Q108325": "religious-christian", // church "Q317557": "religious-christian", // parish church "Q1129743": "religious-christian", // filial church "Q56242275": "religious-christian", // lutheran church "Q623525": "religious-christian", // rotunda "Q131681": "water", // reservoir "Q34627": "religious-jewish", // synagogue "Q47521": "water", // stream "Q2977": "place-of-worship", // cathedral "Q1802963": "home", // cathedral };

// technical constants const maxZoomLevel = 19, defaultMaplinkZoomLevel = 17, defaultMapZoomLevel = 14,

defaultProperties = { 'stroke-width': 2, 'fill-opacity': 0.5 },

indicatorSelector = '.voy-coord-indicator', indicatorCoordsSelector = '.voy-coords a', indicatorGlobeImgSrc = 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Earth_-_The_Noun_Project.svg/20px-Earth_-_The_Noun_Project.svg.png', indicatorMapContainerId = 'voy-topMap',

fullScreenContainerId = 'voy-fullScreenMap',

articlesMapId = 'voy-articles-map', useArticlesMap = true,

mapframeContainerSelector = '.mw-kartographer-container', mapframeMapSelector = '.mw-kartographer-map',

markerSelector = '.vcard', // wrapper selector of a single marker or listing kartographerSelector = '.mw-kartographer-maplink', nameClass = 'listing-name', imageClass = 'listing-image',

footCaptionSelector = '.oo-ui-windowManager-fullscreen .mw-kartographer-captionfoot', captionMarkerClass = 'voy-caption-marker', captionInverseMarkerClass = 'voy-caption-marker-invers',

dataLat = 'data-lat', dataLon = 'data-lon', dataZoom = 'data-zoom', dataName = 'data-name', dataColor = 'data-color', dataSymbol = 'data-symbol', dataNumber = 'data-number', dataGroup = 'data-group-translated', // other wikis: 'data-type' dataDialog = 'data-dialog', dataHeight = 'data-height', dataOverlays = 'data-overlays', fallbackLang = 'en' const maxPOIsPerRequest = 500; // strings depending on page content language const wikiStrings = { de: { defaultShow:     '["Maske","Track","Aktivität","Anderes","Anreise","Ausgehen","Aussicht","Besiedelt","Fehler","Gebiet","Gesundheit","Kaufen","Küche","Natur","Religion","Sehenswert","Unterkunft","aquamarinblau","cosmos","gold","hellgrün","orange","pflaumenblau","rot","silber","violett"]', defaultGroupName: 'Karte', mask:            'Maske', track:           'Track' },			en: { defaultShow:     '["mask","around","buy","city","do","drink","eat","go","listing","other","see","sleep","vicinity","view","black","blue","brown","chocolate","forestgreen","gold","gray","grey","lime","magenta","maroon","mediumaquamarine","navy","orange","plum","purple","red","royalblue","silver","steelblue","teal"]', defaultGroupName: 'map', mask:            'mask', track:           'track' },			es: { defaultShow:     '["máscara","sendero","área","beber","comer","comprar","dormir","error","habitadas","hacer","ir","otro","ver","vista","aguamarina","ciruela","cosmos","oro","lima","naranja","violeta","plata","rojo"]', defaultGroupName: 'mapa', mask:            'máscara', track:           'sendero' },			fr: { defaultShow:     '["aller","destination","diplomatie","loger","manger","sortir","ville","voir","mask","around","buy","city","do","drink","eat","go","listing","other","see","sleep","vicinity","view","black","blue","brown","chocolate","forestgreen","gold","gray","grey","lime","magenta","maroon","mediumaquamarine","navy","orange","plum","purple","red","royalblue","silver","steelblue","teal"]', defaultGroupName: 'carte', mask:            'mask', track:           'piste' },			it: { defaultShow:     '["mask","around","buy","city","do","drink","eat","go","listing","other","see","sleep","vicinity","view","black","blue","brown","chocolate","forestgreen","gold","gray","grey","lime","magenta","maroon","mediumaquamarine","navy","orange","plum","purple","red","royalblue","silver","steelblue","teal"]', defaultGroupName: 'mappa', mask:            'mask', track:           'traccia' }		};

// strings depending on user language const userStrings = { de: { articlesMapTitle:    'Übersicht der Wikivoyage-Artikel', closeButtonTitle:    'Schließen', indicatorActionLabel: 'Karte', indicatorButtonTitle: 'Klick öffnet oder schließt die Karte für $1', magnifyButtonTitle:  'Karte vergrößern', mapCenter:           'Kartenzentrum', mapOf:               'Karte von $1' },			en: { articlesMapTitle:    'Summary of Wikivoyage articles', closeButtonTitle:    'Close', indicatorActionLabel: 'Map', indicatorButtonTitle: 'Click to open or close the map of $1', magnifyButtonTitle:  'Enlarge map', mapCenter:           'Map center', mapOf:               'Map of $1' },			es: { articlesMapTitle:    'Resumen de los artículos de Wikivoyage', closeButtonTitle:    'Cerrar', indicatorActionLabel: 'Mapa', indicatorButtonTitle: 'Haga clic para abrir o cerrar el mapa de $1', magnifyButtonTitle:  'Aumentar mapa', mapCenter:           'Centro del mapa', mapOf:               'Mapa de $1' },			fr: { articlesMapTitle:    'Résumé des articles de Wikivoyage', closeButtonTitle:    'Fermer', indicatorActionLabel: 'Carte', indicatorButtonTitle: 'Cliquez pour ouvrir ou fermer le carte de $1', magnifyButtonTitle:  'Agrandir la carte', mapCenter:           'Centre de la carte', mapOf:               'Carte de $1' },			it: { articlesMapTitle:    'Sommario degli articoli di Wikivoyage', closeButtonTitle:    'Chiudi', indicatorActionLabel: 'Mappa', indicatorButtonTitle: 'Clicca per aprire o chiudere la mappa di $1', magnifyButtonTitle:  'Ingrandisci mappa', mapCenter:           'Centro mappa', mapOf:               'Mappa di $1' }		};

// internal use const ver = '2023-03-29', $body = $( 'body' ), pageLang = mw.config.get( 'wgPageContentLanguage' ), userLang = mw.config.get( 'wgUserLanguage' ), pageTitle = mw.config.get( 'wgTitle' ), articlePath = mw.config.get( 'wgArticlePath' ), thumbPath = '//upload.wikimedia.org/wikipedia/commons/thumb/', scriptUrl = mw.format( 'https://wikivoyage.toolforge.org/w/data/$1-articles.js', pageLang ), isMinerva = mw.config.get( 'skin' ) === 'minerva'; // mobile view var defaultShowArray, messages = {};

// storing prune cluster library var	pruneClusterLib;

// storing GeoJSON data var data = {}; var wikidataResponse = null; var osmResponse = null; var mapHandle;

// array of objects: { name: group.name, attribution: attributions } var groups = [];

// 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 ); }

// creating a Kartographer map function createMap( id, center, zoom, caption, options, color, isInvers ) { mw.loader.using( [ 'ext.kartographer.box' ] ).then( function {				var $id = $( '#' + id ),					group, i, j, 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				} ); mapHandle = map; // 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.enableNearby ) { map.nearbyEnabled = true; if ( options.toggleNearby ) { map.toggleNearby = true; }				}

// adding markers by group names to separate layers if ( options.withData && groups.length ) { for ( i = 0; i < options.show.length; i++ ) { for ( j = 0; j < groups.length; j++ ) { group = groups[ j ]; if ( group.name === options.show[ i ] ) { layerOptions = { name: group.name }; if ( group.attribution !== '' ) { layerOptions.attribution = group.attribution; }								if (data !== null) map.addGeoJSONLayer( data[ group.name ], layerOptions ); break; }						}					}				}				getPOIsFromWD(map); getPOIsFromOSM(map);

// 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 { require( 'ext.kartographer.dialog' ).render( map ); } );					} );				} else { // adding Close control to non-full-screen map if required if ( options.withClose ) { var controls = $( '.leaflet-top.leaflet-right', $id ), control = $( ' ' ) .append( $( '', { title: messages.closeButtonTitle, role: 'button', 'aria-disabled': 'false' } )								.click( function { $id.remove; if ( options.isFullScreen ) { $body.css( { overflow: 'auto' } ); }								} )							);						controls.prepend( control ); }

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

}				map.doWhenReady( function {					map.on('click', function(e){ $("#query-radius-center")[0].value = e.latlng.lat.toFixed(5) + "," + e.latlng.lng.toFixed(5); });					var dragf = function { 						var bounds = map.getBounds;						var center = map.getCenter;						var zoom = map.getZoom;						$("#mapframe-info")[0].textContent = 							"";						$("#mapframe-size")[0].textContent = 							" (size: " + (map.distance(bounds._southWest, bounds._northEast) / 1000).toFixed(1) + "km)";						$("#query-sw")[0].value = bounds._southWest.lat.toFixed(5) + ',' + bounds._southWest.lng.toFixed(5);						$("#query-ne")[0].value = bounds._northEast.lat.toFixed(5) + ',' + bounds._northEast.lng.toFixed(5);						limitMaxQuery;					};					map.on('zoomend', dragf);				   map.on('dragend', dragf);

// remove inert attribute $id.removeAttr( 'inert' );

if ( color && options.withDialog ) { setTimeout( function {							var footCaption = $( footCaptionSelector );							if ( footCaption.length ) {								var	captionArray = footCaption.text.split("​:"),									classes = captionMarkerClass +										( isInvers ? ' ' + captionInverseMarkerClass :  );								footCaption.html( mw.format( '$3 $4',									classes, color, captionArray[ 0 ], captionArray[ 1 ] ||  ) );							}						}, 700); }				} );			} );		}

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

function extractCoordinates(item) { if (item.location && item.location.datatype === "http://www.opengis.net/ont/geosparql#wktLiteral") { var match = item.location.value.match(/Point\(([^ ]+) ([^)]+)\)/);		   if (match && match.length === 3) {		      var lon = parseFloat(match[1].trim);		      var lat = parseFloat(match[2].trim);		      return [lon, lat];		    }		  }		  return null;		}

function convertWDToGeoJSON(wd) { var geoJSON = { type: "FeatureCollection", features: [] };

var bindings = wd.results.bindings; for (var i = 0; i < bindings.length; i++) { var item = bindings[i]; var coordinates = extractCoordinates(item); var markerSymbol = ""; var typeid = ""; if (item.instanceOf.value) { typeid = item.instanceOf.value.split('/').reverse[0]; if (typeid in wdMakiMap) markerSymbol = wdMakiMap[typeid]; }				if (coordinates) { var title = item.placeLabel ? escapeHtml(item.placeLabel.value) : "Untitled"; var wdid = item.place.value.split('/').reverse[0]; var lang = $("#wd-language")[0].value; var imgStr = ""; if (item.image) { var img = "", imgThumb = ""; img = escapeHtml(item.image.value); //imgThumb = img.replace("/commons/", "/commons/thumb/"); //imgThumb += "320px-" + imgThumb.split('/').reverse[0]; imgThumb = img.replace("http://", "https://"); imgStr= "" + "" + " "; }				 var feature = { type: "Feature", geometry: { type: "Point", coordinates: coordinates },				   properties: { 'marker-color': '#999999', 'marker-size': 'medium', "marker-symbol": markerSymbol, "articleName": item.articleTitle ? escapeHtml(item.articleTitle.value) : "", "wikidataID": wdid, "listingName": title, "instanceOf": typeid, title: "" + title + " / " + wdid + " ", description: (item.placeDescription ? (escapeHtml(item.placeDescription.value) + " ") : "") + (item.articleTitle ? (":" + lang + ":" + escapeHtml(item.articleTitle.value) + "") : "No description available") + " " + imgStr }				 };				  geoJSON.features.push(feature); }			}			return geoJSON; }		function convertOSMToGeoJSON(osm) { var geoJSON = { type: "FeatureCollection", features: [] };

var elements = osm.elements; for (var i = 0; i < elements.length; i++) { var item = elements[i]; var coordinates; if ('center' in item) { coordinates = [item.center.lon, item.center.lat]; } else { coordinates = [item.lon, item.lat]; }				var markerSymbol = ""; if ('tourism' in item.tags && item.tags.tourism == 'viewpoint') { markerSymbol = 'viewpoint'; } else if ('tourism' in item.tags && item.tags.tourism == 'museum') { markerSymbol = 'museum'; } else if ('historic' in item.tags) { markerSymbol = item.tags.historic; }

if (coordinates) { var title = 'name' in item.tags ? escapeHtml(item.tags.name) : ""; var feature = { type: "Feature", geometry: { type: "Point", coordinates: coordinates },				   properties: { 'marker-color': '#9999ff', 'marker-size': 'medium', "marker-symbol": markerSymbol, title: title, "listingName": title, description: ' " +				   	' ' + escapeHtml(JSON.stringify(item.tags)).replace(/(Q[0-9]*)/, "$1") + ' '				    }				  };				  geoJSON.features.push(feature);				}			}			return geoJSON;		}

function getAverageViews(lang, articleName) { var d = new Date; d.setMonth(-1); d.setDate(1); var to = d.toISOString.split('T')[0].replaceAll('-', ''); d.setMonth(-6); var from = d.toISOString.split('T')[0].replaceAll('-', ''); var url = "https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/" + lang + ".wikipedia/all-access/all-agents/" + articleName + "/monthly/" + from + "/" + to; return $.ajax({	           url: url,	            method: 'GET',	            dataType: 'json',	        }); }

function retrieveWPViews(lang, geoJSONResult) { var promises = geoJSONResult.features.map(function(entry) {	           var articleName = entry.properties.articleName;	            if (articleName != "" && !articleName.includes('/'))	                return getAverageViews(lang, articleName)	                .then(function(data) { var views = data.items.map(function(item) {return item.views;}); var averageViews = views.length ? (views.reduce(function(a, b) {return (a + b);}) / views.length) : 0; averageViews = Math.ceil(averageViews); entry.properties.views = averageViews; }).catch(function(error) { entry.properties.views = -1;});	       	}); return promises; }		function dumpListings(geoJSONResult) { if (!$('#maptool-listings').length) $('#' + indicatorMapContainerId).after(' '); $('#maptool-listings').append(' ');

var tbody = $('#maptool-listings > table > tbody').last[0]; var lang = $("#wd-language")[0].value;

// Clear existing content tbody.innerHTML = ''; if (geoJSONResult.features.length >= maxPOIsPerRequest) { var tr = document.createElement('tr'); tr.setAttribute("style", "background: red") tr.innerHTML = "More than " + maxPOIsPerRequest + "POIs found, scale down your query "; tbody.append(tr) }

// Loop through items and create rows geoJSONResult.features.forEach(function(item) {	       	var lName = "| name = " + item.properties.listingName;				var lAltName = "";				if (item.properties.articleName && (item.properties.listingName != item.properties.articleName))					lAltName = " | alt = " + item.properties.articleName;	            var tr = document.createElement('tr');				var wparticle = ( item.properties.articleName ? (":" + lang + ":" + item.properties.articleName + "") : "No title available");				var wdStr = "";				var coordsStr = "";				if (item.properties.wikidataID)					wdStr = " | wikidata = " + item.properties.wikidataID;				else					coordsStr = " | lat=" + item.geometry.coordinates[1] + " | long=" + item.geometry.coordinates[0];	           tr.innerHTML = "\	                " + item.properties.title + " \	                " + wparticle + " \	                " + (item.properties.views ? item.properties.views : "") + " \	               " + item.properties['marker-symbol'] + " | " + item.properties.instanceOf + "</a> \	                 ";	            tbody.appendChild(tr);	        }); }

function addMarkerColors(geoJSONResult) { var redShade; var redFrom = 0xff; var redTo = 0x99;

for (var i = 0; i < geoJSONResult.features.length; i++) { redShade = ((geoJSONResult.features.length - i) * (redFrom - redTo) / geoJSONResult.features.length) + redTo; var redShadeStr = redShade.toString(16); if (redShadeStr.length == 1) redShadeStr = "0" + redShadeStr; if (geoJSONResult.features[i].properties.views >= 0) geoJSONResult.features[i].properties['marker-color'] = '#' + redShadeStr + '9999'; }		}

function getPOIsFromOSM(map) { if (!osmResponse) return; var geoJSONResult = convertOSMToGeoJSON(osmResponse); map.addGeoJSONLayer(geoJSONResult, {name: "OSM items"}); if ($("#osm-enable")[0].checked) { dumpListings(geoJSONResult); }		}

function getWDGeoJSON(lat, lon) { function makeSPARQLQuery( endpointUrl, sparqlQuery ) { var settings = { headers: { Accept: 'application/sparql-results+json' }, data: { query: sparqlQuery } };				return $.ajax( endpointUrl, settings ); }			var radius = $("#query-radius-km")[0].value; var lang = $("#wd-language")[0].value; var auxFilter = $("#wd-filter")[0].value; var wikirule = (lang !== "") ? "?article schema:about ?place . \			 ?article schema:isPartOf <https://" + lang + ".wikipedia.org/>. \			  ?article schema:name ?articleTitle ." : "";			if (lang === "") lang = "en";

if (!$("#wd-filter-religious")[0].checked) { auxFilter += " MINUS{?place wdt:P31/wdt:P279* wd:Q24398318} "; // religious building }			if (!$("#wd-filter-settlements")[0].checked) { auxFilter += " MINUS{?place wdt:P31/wdt:P279? wd:Q486972} "; // human settlement auxFilter += " MINUS{?place wdt:P31/wdt:P279* wd:Q15284} "; // municipality auxFilter += " MINUS{?place wdt:P31/wdt:P279? wd:Q56061} "; // administrative division }			var areaQuery; if ($("#query-radius")[0].checked) { areaQuery = "SERVICE wikibase:around { \  ?place wdt:P625 ?location . \   bd:serviceParam wikibase:center \"Point(" + String(lon) + "," + String(lat) + ")\"^^geo:wktLiteral . \   bd:serviceParam wikibase:radius \"" + String(radius) + "\" . \ }"; } else { var sw = $("#query-sw")[0].value.split(","); var ne = $("#query-ne")[0].value.split(","); areaQuery = "SERVICE wikibase:box { \  ?place wdt:P625 ?location . \   bd:serviceParam wikibase:cornerSouthWest \"Point(" + String(sw[1]) + "," + String(sw[0]) + ")\"^^geo:wktLiteral .\   bd:serviceParam wikibase:cornerNorthEast \"Point(" + String(ne[1]) + "," + String(ne[0]) + ")\"^^geo:wktLiteral .\ }"; }

var endpointUrl = 'https://query.wikidata.org/sparql', sparqlQuery = "\n" + "\ SELECT DISTINCT ?place ?placeDescription ?placeLabel ?articleTitle \ \ (SAMPLE(?location) as ?location) \ (SAMPLE(?image) AS ?image)\ (SAMPLE(?instanceOf) AS ?instanceOf)\ \ WHERE {\ " + areaQuery + auxFilter + "\ SERVICE wikibase:label { bd:serviceParam wikibase:language \"en," + lang + "\". } \ ?place wdt:P18 ?image. \  ?place wdt:P31 ?instanceOf. \  " + wikirule + "\ } \ GROUP BY ?place ?placeDescription ?placeLabel ?articleTitle \ LIMIT " + maxPOIsPerRequest + " \ ";

return makeSPARQLQuery( endpointUrl, sparqlQuery).then(function(data){wikidataResponse = data;}); }

function getPOIsFromWD(map) { if (!wikidataResponse) return; var geoJSONResult = convertWDToGeoJSON(wikidataResponse); if ($("#wd-sort-views")[0].checked) { var lang = $("#wd-language")[0].value; var promises = retrieveWPViews(lang, geoJSONResult);

Promise.all( promises ).then( function {		           // Sort the table data by views in descending order		            geoJSONResult.features.sort(function (a, b) {return (b.properties.views || 0) - (a.properties.views || 0);});		            addMarkerColors(geoJSONResult);					map.addGeoJSONLayer(geoJSONResult, {name: "wikidata items"});					if ($("#wd-table")[0].checked) {		            	dumpListings(geoJSONResult);					}		        }); } else { map.addGeoJSONLayer(geoJSONResult, {name: "wikidata items"}); if ($("#wd-table")[0].checked) { dumpListings(geoJSONResult); }			}		}

function getOSMGeoJSON(lat, lon) { function makeOSMQuery( endpointUrl, query ) { var settings = { headers: { Accept: 'application/json' }, data: { data: query } // encodeURIComponent(query) };				return $.ajax( endpointUrl, settings ); }			var auxFilter = ""; var bbox, jsonreq; var nodeFilterBase, wayFilterBase; if ($("#query-radius")[0].checked) { var radius = $("#query-radius-km")[0].value; nodeFilterBase = "node(around:" + String(radius * 1000) + ", " + String(lat) + ", " + String(lon) + ")"; wayFilterBase = "way(around:" + String(radius * 1000) + ", " + String(lat) + ", " + String(lon) + ")"; bbox = ""; jsonreq = '[out:json];' } else { bbox = "[bbox:" + $("#query-sw")[0].value + "," + $("#query-ne")[0].value + "];"; jsonreq = '[out:json]' nodeFilterBase = "node"; wayFilterBase = "way"; }			if ($("#osm-filter")[0].value != "") { var f = $("#osm-filter")[0].value; auxFilter += nodeFilterBase + f + ";"; auxFilter += wayFilterBase + f + ";"; }

if ($("#osm-filter-historic")[0].checked) { auxFilter += nodeFilterBase + '["historic"~"."]["name"~"."];'; auxFilter += wayFilterBase + '["historic"~"."]["name"~"."];'; }			if ($("#osm-filter-viewpoint")[0].checked) { auxFilter += nodeFilterBase + '["tourism"~"viewpoint"];'; auxFilter += wayFilterBase + '["tourism"~"viewpoint"];'; }			if ($("#osm-filter-museum")[0].checked) { auxFilter += nodeFilterBase + '["tourism"~"museum"];'; auxFilter += wayFilterBase + '["tourism"~"museum"];'; }

var endpointUrl = 'https://overpass-api.de/api/interpreter', overpassQuery = jsonreq + bbox + "( " + auxFilter + "); out " + maxPOIsPerRequest + " tags center;";

return makeOSMQuery( endpointUrl, overpassQuery).then(function(data){osmResponse = data;}); }

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

promise = $.ajax( { // instead of $.getJSON				dataType: 'json',   			url: obj.url,    			timeout: 3000			} ).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 = defaultProperties; }						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++ ) { feature = obj.features[ i ]; if ( $.isEmptyObject( feature.properties ) ) { feature.properties = properties; } else { feature.properties = $.extend( {}, properties, feature.properties ); }							}						}				}			}, function { // failed. Do nothing. } );

return promise; }

// Creating attribution strings function getAttribution( obj ) { var uri = new mw.Uri( obj.url ), 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 function getKartographerLiveData { var group, i, obj, promiseArray = [], attributions, link;

data = mw.config.get( 'wgKartographerLiveData' ); if ( data ) { groups = []; // start with empty global array for ( group in data ) { // ignoring empty groups if ( data[ group ].length ) { attributions = []; for ( i = 0; i < data[ group ].length; i++ ) { obj = data[ group ][ i ]; // expand external data if ( obj.type === 'ExternalData' && obj.url ) { promiseArray.push( getGeoJSON( obj ) ); link = getAttribution( obj ); if ( link !== '' ) { attributions.push( link ); }							}						}						attributions = attributions.join( ', ' ); groups.push( { name: group, attribution: attributions } ); }				}			}			// wait for getting all external data // regardless of failures, addMapTools will be executed if ( typeof Promise !== 'undefined' ) { Promise.all( promiseArray ) .then( function {						addMapTools;					} ) // initialization also in case of failures // maybe external data are not shown .catch( function {						addMapTools;					} ); } else { addMapTools; // for really old browsers }		}

// getting all vCard/listing and marker information from article function getPOIsFromArticle { // 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

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

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

markers.each( function {				$this = $( this );				link = $( kartographerSelector, $this ).first;				if ( link.length ) {					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 symbol = $this.attr( dataSymbol ); if (symbol && symbol.charAt( 0 ) === '-' ) { symbol = link.text; }

// getting title title = $( '.' + nameClass, $this ).first; clone = title.clone; $( '.image', clone ).remove; // remove images from title wikiLink = $( 'a', clone ).first; clone.remove; title = ( wikiLink.length ) ? wikiLink[ 0 ].outerHTML : $this.attr( dataName );

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

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

groups = []; // start with empty array for ( group in data ) { groups.push( { name: group, attribution: '' } ); }		}

// returning zoom parameter string as a valid number function getZoom( s, defaultValue ) { var zoom = ( typeof s == 'string' ) ? parseInt( s ) : -1; if ( zoom < 0 || zoom > maxZoomLevel ) { return defaultValue || defaultMapZoomLevel; }			return zoom; }

function makeContainer( id, style="height: 500px" ) { return $( ' ', { id: id, role: 'dialog', 'data-ver': ver, style: style } ); }

// displaying a map by clicking the geo-indicator button function indicatorMap { var indicator = $( indicatorSelector ).first; if ( !indicator.length ) { return; }			$( indicatorCoordsSelector ).attr( 'target', '_blank' );

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

var lat, lon, zoom, center; var coords = getArticleCoords; lat = coords[0]; lon = coords[1]; zoom = coords[2]; // var zoom = getZoom( indicator.attr( dataZoom ) ), // 	lat = indicator.attr( dataLat ), // 	lon = indicator.attr( dataLon ), center = [ lat, lon ];

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

// add or modify indicator action buttons var mapTitle = mw.format( messages.mapOf, pageTitle ); if ( isMinerva ) { // mobile view // add indicator action button and event handler

var indicatorImg = $( ' ', {					src: indicatorGlobeImgSrc,					width: '20', height: '20'				} ); indicator = $( '<a>', {						id: 'mw-indicator-i3-geo',						title: mw.format( messages.indicatorButtonTitle, pageTitle ),						class: 'mw-indicator', href: '#'					} ) .css( { display: 'inline-block' } ) .append( indicatorImg ) .append( document.createTextNode( ' ' + messages.indicatorActionLabel ) ); indicator = $( '<li>', {						id: 'page-actions-i3-geo',						class: 'page-actions-menu__list-item'					} ) .append( indicator ) .click( function {						if (mapHandle) {							var oldcenter = mapHandle.getCenter;							center = [oldcenter.lat, oldcenter.lng]							zoom = mapHandle.getZoom;						}						var container = $( '#' + id );						if ( container.length ) {							container.remove;							if ($("#maptool-listings").length != 0)								$("#maptool-listings")[0].remove;						}						{							$( '#bodyContent' ).prepend( makeContainer( id ) );							$('#' + id).css("resize", ($("#wd-map-resizable")[0].checked ? "both" : "none"));							var qcenter = $("#query-radius-center")[0].value.split(',');							var qlat = parseFloat(qcenter[0]);							var qlon = parseFloat(qcenter[1]);

wikidataResponse = null; osmResponse = null; var promiseArray = []; if ($("#wd-enable")[0].checked) promiseArray.push(getWDGeoJSON(qlat, qlon)); if ($("#osm-enable")[0].checked) promiseArray.push(getOSMGeoJSON(qlat, qlon)); Promise.all( promiseArray ) .then( function {								createMap( id, center, zoom, mapTitle, options );							} ) }					} );				$( '#page-actions #page-actions-edit' ).after( indicator );			} else { // desktop views				// replace indicator image and add an event handler

$( indicatorSelector + ' .voy-map-globe-default' ) .css( { display: 'none' } ); $( indicatorSelector + ' .voy-map-globe-js' ) .css( { display: 'inline', cursor: 'pointer' } ) .attr( 'title', mw.format( messages.indicatorButtonTitle, pageTitle ) ) .click( function {						var style = "height: 500px";						if (mapHandle) {							var oldcenter = mapHandle.getCenter;							center = [oldcenter.lat, oldcenter.lng]							zoom = mapHandle.getZoom;							style = "width: " + $("#" + id).width + "px;" +									"height: " + $("#" + id).height + "px;";						}						var container = $( '#' + id );						if ( container.length ) {							container.remove;							if ($("#maptool-listings"))								$("#maptool-listings")[0].remove;						}						{							$( '#contentSub' ).after( makeContainer( id, style ) );							$('#' + id).css("resize", ($("#wd-map-resizable")[0].checked ? "both" : "none"));							var qcenter = $("#query-radius-center")[0].value.split(',');							var qlat = parseFloat(qcenter[0]);							var qlon = parseFloat(qcenter[1]);

wikidataResponse = null; osmResponse = null; var promiseArray = []; if ($("#wd-enable")[0].checked) promiseArray.push(getWDGeoJSON(qlat, qlon)); if ($("#osm-enable")[0].checked) promiseArray.push(getOSMGeoJSON(qlat, qlon)); Promise.all( promiseArray ) .then( function {								createMap( id, center, zoom, mapTitle, options );							} ); }					} );			}		}

// returning show parameter string as an array function getShow( s ) { return ( s ) ? JSON.parse( s ) : defaultShowArray; }

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

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

var center, color, isInvers, lat, lon, name, symbolText, target, wrapper, zoom, $this;

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

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

$this.click( function( event ) {					event.stopImmediatePropagation;					event.preventDefault;

// marker could contain an image -> closest target = $( event.target ).closest( kartographerSelector ); wrapper = target.closest( markerSelector );

lat = target.attr( dataLat ); lon = target.attr( dataLon ); center = [ lat, lon ]; zoom = getZoom( target.attr( dataZoom ), defaultMaplinkZoomLevel );

name = wrapper.attr( dataName ) || ''; symbolText = target.text; if ( symbolText !== '' ) { color = wrapper.attr( dataColor ); isInvers = target.closest( '.listing-map-inverse' ).length; }					if ( name === '' ) { name = symbolText; } else if ( name !==  && symbolText !==  ) { name = symbolText + '​: ' + name; }

options.show = getShow( target.attr( dataOverlays ) );

$body.append( makeContainer( id ) ); createMap( id, center, zoom, name, options, color, isInvers );

return false; // don't follow the link } );			} );		}

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

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

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

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

// no magnify button if zoom is already maxZoomLevel // not in frameless mode map = $( mapframeMapSelector, $this ).first; caption = $( '.thumbcaption', $this ).first; zoom = getZoom( map.attr( dataZoom ) ); if ( map.length && caption.length && zoom < maxZoomLevel ) { link = $( ' </a>' ) .css( { cursor: 'pointer' } ) .attr( 'title', messages.magnifyButtonTitle ) .click( function( event ) {							target = $( event.target );							map = target.closest( mapframeContainerSelector );							caption = $( '.thumbcaption', map ).first;							name = caption.text;

// getting initial position from data if lat or lon // or zoom are undefined map = $( mapframeMapSelector, 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 = getShow( map.attr( dataOverlays ) );

$body.append( makeContainer( id ) ); createMap( id, center, zoom, name, options ); } );					caption.prepend( $( ' ' ).append( link ) );				}			} ); }

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

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

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

function togglePopup(id, childs) { var next = $(id).is(":hidden"); childs.push(id); childs.forEach(function (i) { if (next) {$(i).show;} else {$(i).hide;} }); }		function escapeHtml(text) { var escapedText = { '&': '&a;', //'"': '&q;', // this should be fine, we don't do eval		   //'\'': '&s;',		    '<': '&l;',		    '>': '&g;',		  };		  //return text.replace(/[&"'<>]/g, function(s) {return escapedText[s];}); return text.replace(/[&<>]/g, function(s) {return escapedText[s];}); }		function getArticleCoords { // try to use var zoom = 13; var lat = RLCONF['wgCoordinates']['lat']; var lon = RLCONF['wgCoordinates']['lon']; // override by first mapframe if ($('.mw-kartographer-map').length > 0) { var firstmap = $('.mw-kartographer-map')[0]; var v;				v = firstmap.getAttribute('data-lat'); if (v != null) lat = v;				v = firstmap.getAttribute('data-lon'); if (v != null) lon = v;				v = firstmap.getAttribute('data-zoom'); if (v != null) zoom = v;			} return [lat, lon, zoom]; }

function getDistance(origin, destination) { // return distance in meters; we don't always have the leaflet map, so we resort to this... var lon1 = toRadian(origin[1]), lat1 = toRadian(origin[0]), lon2 = toRadian(destination[1]), lat2 = toRadian(destination[0]); var deltaLat = lat2 - lat1; var deltaLon = lon2 - lon1; var a = Math.pow(Math.sin(deltaLat/2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(deltaLon/2), 2); var c = 2 * Math.asin(Math.sqrt(a)); var EARTH_RADIUS = 6371; return c * EARTH_RADIUS * 1000; }		function toRadian(degree) { return degree*Math.PI/180; }		function limitMaxQuery { var disabled = false; try { if ($("#query-radius")[0].checked) { disabled = $("#query-radius-km")[0].value > 100; } else{ var sw = $("#query-sw")[0].value.split(","); var ne = $("#query-ne")[0].value.split(","); var distance = getDistance(sw, ne); if (isNaN(distance) || distance > 200000) disabled = true; }			} catch (e) { disabled = true; }			$(".voy-map-globe-js").prop("disabled", disabled); }		function addMainQueryDialog { var maptoolStr=`\ \			`;		   function enableForm(chkbox) {			   chkbox.parent.parent.find('input').not(chkbox).prop(			        'disabled', !$(chkbox).prop('checked')			    ); }			if ('wgCoordinates' in RLCONF) { var coords = getArticleCoords; $('.mw-indicators').prepend(			   	' \			    		 \			    			<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/32px-Wikidata-logo.svg.png"/> \			    	  '		    	); $('#maptool-button').click(		   		function {togglePopup('#maptool', ['#maptool-listings', '#voy-topMap']);}); $("#contentSub").append(maptoolStr); $("#query-radius-center")[0].value = coords[0] + "," + coords[1]; $("#wd-map-resizable").change(function {		   		$("#voy-topMap").css("resize", (this.checked ? "both" : "none"));}); enableForm($('#osm-enable')) $('#osm-enable').change(function { enableForm($(this)); }); enableForm($('#wd-enable')) $('#wd-enable').change(function { enableForm($(this)); }); $('#query-radius').change(function { limitMaxQuery; }); $('#query-radius-km').change(function { limitMaxQuery; }); $('#query-area').change(function { limitMaxQuery; }); $('#query-sw').change(function { limitMaxQuery; }); $('#query-ne').change(function { limitMaxQuery; }); }		}

// adding all tools // called by getKartographerLiveData function addMapTools { // groups array is set by getKartographerLiveData // if groups array is empty try to get data from article if ( !groups.length ) { getPOIsFromArticle; }

addMainQueryDialog; //addMagnifyButton; indicatorMap; //replaceMaplinks; //if ( useArticlesMap ) { //	articlesMap; //}		}

function init { setupMessages; defaultShowArray = JSON.parse( messages.defaultShow ), getKartographerLiveData; // calling addMapTools }

return { init: init }; } ;

$( mapTools.init );

} ( jQuery, mediaWiki ) );

//