MediaWiki:Gadget-Maptool.js

From Wikivoyage
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*
A tool based on the original code as stated below, that helps searching for
POIs and creating dynamic maps.
*/
//<nowiki>
/*****************************************************************************
 * 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 = $( '<div class="leaflet-bar leaflet-control-static leaflet-control"></div>' )
							.append( $( '<a class="voy-icon-close"></a>',
									{ 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|" + center.lat.toFixed(5) + "|" + center.lng.toFixed(5) +
							"|zoom=" + zoom +
							"|width=" + $("#voy-topMap").width() +
							"|height=" + $("#voy-topMap").height() + "}}";
						$("#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( '<span class="$1" style="background-color: $2">$3</span>$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= "<a href=\"" + img + "\">" + 
				  		"<img style=\"width: 100%\" src=\"" + imgThumb + "\" />"
				  		+ "</a><br/> ";
				  }
				  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: "<a href=\"" + escapeHtml(item.place.value) + "\">" + title + " / " + wdid + "</a><br/> ",
				      description:
				        (item.placeDescription ? (escapeHtml(item.placeDescription.value) + "<br/>") : "") +
				    	(item.articleTitle ? ("<a href=\"https://" + lang + ".wikipedia.org/wiki/" + escapeHtml(item.articleTitle.value) + "\">:" + lang + ":" + escapeHtml(item.articleTitle.value) + "</a>") : "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:
				    	'{{listing | name=' + title + ' | lat=' + coordinates[1] + " | long=" + coordinates[0] + "}}<br/>" +
				    	'<div style="width: 250px; word-break: break-all">' + escapeHtml(JSON.stringify(item.tags)).replace(/(Q[0-9]*)/, "<a href=\"https://www.wikidata.org/wiki/$1\">$1</a>") + '</div>'
				    }
				  };
				
				  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('<div id="maptool-listings"></div>');
			$('#maptool-listings').append('<table border="1"><thead><tr><th>ID</th><th>WP name</th><th>views</th><th>symbol</th><th>listing</th></tr></thead><tbody></tbody></table>');

			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 = "<td colspan='5'>More than " + maxPOIsPerRequest + "POIs found, scale down your query</td>";
				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 ?
					("<a href=\"https://" + lang + ".wikipedia.org/wiki/" + item.properties.articleName + "\">:" + lang + ":" + item.properties.articleName + "</a>")
					: "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 = "\
	                <td>" + item.properties.title + "</td>\
	                <td>" + wparticle + "</td>\
	                <td>" + (item.properties.views ? item.properties.views : "") + "</td>\
	                <td>" + item.properties['marker-symbol'] + " | <a href=\"https://www.wikidata.org/wiki/" + item.properties.instanceOf + "\">" + item.properties.instanceOf + "</a></td>\
	                <td>{{listing " + lName + lAltName + wdStr + coordsStr + "}}</td>";
	            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( '<noscript>', '' ).replace( '</noscript>', '' );
					}

					// 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 $( '<div></div>', { 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 = $( '<img>', {
					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 class="internal"></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( $( '<div class="magnify"></div>' ).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 {{geo}}
			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=`\
			<table id="maptool" style="display: none; border: 1px solid black; width: 100%;background-color: #eee;"><tbody>\
			<tr><td colspan="2">Show:\
			    <input type="checkbox" id="wd-map" checked="" disabled title="Show map with the items.">\
			    <label for="wd-map">map</label>\
			    (<input type="checkbox" id="wd-map-resizable" title="Enable resize handle for the map">\
			    <label for="wd-map-resizable">resizable</label>) \
			    <input type="checkbox" id="wd-table" checked="" title="Show a table containing the found data. If Wikipedia language is specified, also the respective wikipedia articles in that language.">\
			    <label for="wd-table">table</label>\
			    | <a href="https://en.wikivoyage.org/wiki/Wikivoyage:Maptool">help</a> \
			</td></tr>\
			<tr><td colspan="2" style="border-bottom: 1px solid black"><b>Query area:</b>\
			    <br/><span style="white-space: nowrap"><input type="radio" id="query-radius" name="query-type" checked /><label for="query-radius">Radius/km:</label>\
			    <input type="text" id="query-radius-km" value="10" style="width: 5em; box-sizing: border-box" title="Radius." />, center:<input type="text" id="query-radius-center" style="width: 20em; box-sizing: border-box" title="map center (lat,long)" /></span>\
			    <br/><span style="white-space: nowrap"><input type="radio" id="query-area" name="query-type" /><label for="query-area">Area:</label>\
			    SW:<input type="text" id="query-sw" style="width: 20em; box-sizing: border-box" title="SW corner (lat,long)" />\
			    NE:<input type="text" id="query-ne" style="width: 20em; box-sizing: border-box" title="NE corner (lat,long)" /></span>\
			</td></tr>\
			<tr style="vertical-align:  top;"><td id="wd-cfg" style="border-right: 1px solid #ccc; padding: 0.5em; width: 50%">\
			    <h3><input type="checkbox" id="wd-enable" checked><label for="wd-enable">Wikidata</label></h3>\
			    <label for="wd-language">Wikipedia language:</label><input type="text" id="wd-language" class="wd-input" value="en" style="width: 5em; box-sizing: border-box" title="Enter wikipedia language shortcut (two or three letters). If left empty, just wikidata items are searched, with or without wikipedia articles.">\
			    <br /><input type="checkbox" id="wd-sort-views" checked="" title="If checked AND wikipedia article is found (see above), the average number of views in the past months is retrieved, and used to sort the items (descending)."><label for="wd-sort-views">Sort by wikipedia viewcount</label>\
			\
			    <br/><br/><div style="margin-left: 1em; margin-left: 1em; background: #dddddd; padding: 0.25em;">\
			        <b>Filters</b><br/><input type="checkbox" class="wd-input" id="wd-filter-religious" title="Show religious buildings"><label for="wd-filter-religious">religious</label>\
			        <br><input type="checkbox" class="wd-input" id="wd-filter-settlements" title="Show human settlements buildings"><label for="wd-filter-settlements">settlements</label>\
			        <br><label for="wd-filter">SPARQL:</label><input type="text" id="wd-filter" class="wd-input" value="MINUS{?place wdt:P31/wdt:P279* wd:Q3839081}" style="width: 100%;box-sizing: border-box" title="Filters wikidata using also this SPARQL query - make sure the syntax is correct, if not empty. The default/example filters out all disaster events."></div>\
			</td><td id="osm-cfg" style="padding: 0.5em">\
			    <h3><input type="checkbox" id="osm-enable"><label for="osm-enable"> OpenStreetMap</label></h3>\
			    <div style="margin-left: 1em; margin-left: 1em; background: #dddddd; padding: 0.25em;">\
			        <b>Filters</b><br/><input type="checkbox" id="osm-filter-viewpoint" checked=""><label for="osm-filter-viewpoint">viewpoint</label>\
			        <br><input type="checkbox" id="osm-filter-historic" checked=""><label for="osm-filter-historic">historic</label>\
			        <br><input type="checkbox" id="osm-filter-museum" checked=""><label for="osm-filter-museum">museum</label>\
			        <br><label for="osm-filter">AUX:</label><input type="text" id="osm-filter" value="[\'tourism\'~\'attraction\']" style="width: 100%;box-sizing: border-box" title="Filters OSM also uing this tags - make sure the syntax is correct, if not empty.">\
			    </div>\
			</td></tr>\
			\
			<tr><td class="voy-coord-indicator" style="background: white">\
			    <button class="voy-map-globe-js" style="display: inline; cursor: pointer;" title="Click to open or close the map of Ruzomberok">Query and show</button>\
			    <span id="mapframe-size"></span> \
			</td><td id="mapframe-info" style="text-align: right; background: white"></td></tr>\
			</tbody></table>\
			`;
		    
		    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(
			    	'<span id="maptool-button"> \
			    		<span style="display: inline; cursor: pointer;"> \
			    			<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/32px-Wikidata-logo.svg.png"/> \
			    	</span></span>'
		    	);
		    	$('#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 ) );

//</nowiki>