MediaWiki:Gadget-MapTools.js
Nota: Después de publicar, quizás necesite actualizar la caché de su navegador para ver los cambios.
- Firefox/Safari: Mantenga presionada la tecla Shift mientras pulsa el botón Actualizar, o presiona Ctrl+F5 o Ctrl+R (⌘+R en Mac)
- Google Chrome: presione Ctrl+Shift+R (⌘+Shift+R en Mac)
- Edge: mantenga presionada Ctrl mientras pulsa Actualizar, o presione Ctrl+F5
//<nowiki>
/*****************************************************************************
* mapTools v2.1, 2023-10-17
* 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() {
// 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',
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';
// 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: {
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: {
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: {
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: {
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: {
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 GeoJSON data
var data = {};
// 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 = require( 'ext.kartographer.box' );
var map = kartoBox.map( {
container: $id[ 0 ],
center: center,
zoom: zoom,
allowFullScreen: options.allowFullScreen,
alwaysInteractive: true,
captionText: caption,
fullscreen: options.isFullScreen,
featureType: options.featureType
} );
// following line is necessary for proper loading of
// map-dialog sidebar
map.initView( center, zoom );
// 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;
}
map.addGeoJSONLayer( data[ group.name ], layerOptions );
break;
}
}
}
}
// 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() {
// 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 ]
}
} );
}
// 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.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 ) {
return $( '<div></div>', { id: id, role: 'dialog', 'data-ver': ver } );
}
// 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 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() {
var container = $( '#' + id );
if ( container.length ) {
container.remove();
} else {
$( '#bodyContent' ).prepend( makeContainer( id ) );
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 container = $( '#' + id );
if ( container.length ) {
container.remove();
} else {
$( '#contentSub' ).after( makeContainer( id ) );
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 ) );
}
} );
}
// 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();
}
addMagnifyButton();
indicatorMap();
replaceMaplinks();
}
function init() {
setupMessages();
defaultShowArray = JSON.parse( messages.defaultShow ),
getKartographerLiveData(); // calling addMapTools()
}
return { init: init };
} ();
$( mapTools.init );
} ( jQuery, mediaWiki ) );
//</nowiki>