| Current File : /home/jvzmxxx/wiki1/extensions/Kartographer/modules/box/Map.js |
/* globals module, require */
/**
* # Kartographer Map class.
*
* Creates a map with layers, markers, and interactivity.
*
* @alias KartographerMap
* @class Kartographer.Box.MapClass
* @extends L.Map
*/
module.Map = ( function ( mw, OpenFullScreenControl, dataLayerOpts, ScaleControl, DataManager, topojson, window, undefined ) {
var scale, urlFormat,
mapServer = mw.config.get( 'wgKartographerMapServer' ),
worldLatLng = new L.LatLngBounds( [ -90, -180 ], [ 90, 180 ] ),
Map,
precisionPerZoom = [ 0, 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5 ],
inlineDataLayerKey = 'kartographer-inline-data-layer',
inlineDataLayerId = 0;
function bracketDevicePixelRatio() {
var i, scale,
brackets = mw.config.get( 'wgKartographerSrcsetScales' ),
baseRatio = window.devicePixelRatio || 1;
if ( !brackets ) {
return 1;
}
brackets.unshift( 1 );
for ( i = 0; i < brackets.length; i++ ) {
scale = brackets[ i ];
if ( scale >= baseRatio || ( baseRatio - scale ) < 0.1 ) {
return scale;
}
}
return brackets[ brackets.length - 1 ];
}
scale = bracketDevicePixelRatio();
scale = ( scale === 1 ) ? '' : ( '@' + scale + 'x' );
urlFormat = '/{z}/{x}/{y}' + scale + '.png';
L.Map.mergeOptions( {
sleepTime: 250,
wakeTime: 500,
sleepNote: false,
sleepOpacity: 1,
// the default zoom applied when `longitude` and `latitude` were
// specified, but zoom was not.å
fallbackZoom: 13
} );
L.Popup.mergeOptions( {
minWidth: 160,
maxWidth: 300,
autoPanPadding: [ 12, 12 ]
} );
/* eslint-disable no-underscore-dangle */
/**
* Validate that the bounds contain no outlier.
*
* An outlier is a layer whom bounds do not fit into the world,
* i.e. `-180 <= longitude <= 180 && -90 <= latitude <= 90`
*
* There is a special case for **masks** (polygons that cover the entire
* globe with a hole to highlight a specific area). In this case the
* algorithm tries to validate the hole bounds.
*
* @param {L.Layer} layer Layer to get and validate the bounds.
* @return {L.LatLng|boolean} Bounds if valid.
* @private
*/
function validateBounds( layer ) {
var bounds = ( typeof layer.getBounds === 'function' ) && layer.getBounds();
bounds = bounds || ( typeof layer.getLatLng === 'function' ) && layer.getLatLng();
if ( bounds && worldLatLng.contains( bounds ) ) {
return bounds;
} else if ( layer instanceof L.Polygon && layer._holes && layer._holes[ 0 ] ) {
bounds = new L.LatLngBounds( layer._convertLatLngs( layer._holes[ 0 ] ) );
if ( worldLatLng.contains( bounds ) ) {
return bounds;
}
}
return false;
}
/**
* Gets the valid bounds of a map/layer.
*
* @param {L.Map|L.Layer} layer
* @return {L.LatLngBounds} Extended bounds
* @private
*/
function getValidBounds( layer ) {
var layerBounds = new L.LatLngBounds();
if ( typeof layer.eachLayer === 'function' ) {
layer.eachLayer( function ( child ) {
layerBounds.extend( getValidBounds( child ) );
} );
} else {
layerBounds.extend( validateBounds( layer ) );
}
return layerBounds;
}
Map = L.Map.extend( {
/**
* @constructor
* @param {Object} options **Configuration and options:**
* @param {HTMLElement} options.container **Map container.**
* @param {boolean} [options.allowFullScreen=false] **Whether the map
* can be opened in a full screen dialog.**
* @param {string[]} [options.dataGroups] **List of known data groups,
* fetchable from the server, to add as overlays onto the map.**
* @param {Object|Array} [options.data] **Inline GeoJSON features to
* add to the map.**
* @param {boolean} [options.alwaysInteractive=false] Prevents the map
* from becoming static when the screen is too small.
* @param {Array|L.LatLng} [options.center] **Initial map center.**
* @param {number} [options.zoom] **Initial map zoom.**
* @param {string} [options.style] Map style. _Defaults to
* `mw.config.get( 'wgKartographerDfltStyle' )`, or `'osm-intl'`._
* @param {Kartographer.Box.MapClass} [options.parentMap] Parent map
* _(internal, used by the full screen map to refer its parent map)_.
* @param {boolean} [options.fullscreen=false] Whether the map is a map
* opened in a full screen dialog _(internal, used to indicate it is
* a full screen map)_.
* @param {string} [options.fullScreenRoute] Route associated to this map
* _(internal, used by "`<maplink>`" and "`<mapframe>`")_.
* @member Kartographer.Box.MapClass
*/
initialize: function ( options ) {
var args,
style = options.style || mw.config.get( 'wgKartographerDfltStyle' ) || 'osm-intl',
map = this;
if ( options.center === 'auto' ) {
options.center = undefined;
}
if ( options.zoom === 'auto' ) {
options.zoom = undefined;
}
$( options.container ).addClass( 'mw-kartographer-interactive' );
args = L.extend( {}, L.Map.prototype.options, options, {
// `center` and `zoom` are to undefined to avoid calling
// setView now. setView is called later when the data is
// loaded.
center: undefined,
zoom: undefined
} );
L.Map.prototype.initialize.call( this, options.container, args );
/**
* @property {jQuery} $container Reference to the map
* container.
* @protected
*/
this.$container = $( this._container );
this.on( 'kartographerisready', function () {
// eslint-disable-next-line camelcase
map._kartographer_ready = true;
} );
/**
* @property {Kartographer.Box.MapClass} [parentMap=null] Reference
* to the parent map.
* @protected
*/
this.parentMap = options.parentMap || null;
/**
* @property {Kartographer.Box.MapClass} [parentLink=null] Reference
* to the parent link.
* @protected
*/
this.parentLink = options.parentLink || null;
/**
* @property {string} The feature type identifier.
* @protected
*/
this.featureType = options.featureType;
/**
* @property {Kartographer.Box.MapClass} [fullScreenMap=null] Reference
* to the child full screen map.
* @protected
*/
this.fullScreenMap = null;
/**
* @property {boolean} useRouter Whether the map uses the Mediawiki Router.
* @protected
*/
this.useRouter = !!options.fullScreenRoute;
/**
* @property {string} [fullScreenRoute=null] Route associated to this map.
* @protected
*/
this.fullScreenRoute = options.fullScreenRoute || null;
/**
* @property {string} [captionText=''] Caption associated to the map.
* @protected
*/
this.captionText = options.captionText || '';
/**
* @property {Object} dataLayers References to the data layers.
* @protected
*/
this.dataLayers = {};
/* Add base layer */
/**
* @property {L.TileLayer} wikimediaLayer Reference to `Wikimedia`
* tile layer.
* @protected
*/
this.wikimediaLayer = L.tileLayer( mapServer + '/' + style + urlFormat, {
maxZoom: 18,
attribution: mw.message( 'kartographer-attribution' ).parse()
} ).addTo( this );
/* Add map controls */
/**
* @property {L.Control.Attribution} attributionControl Reference
* to attribution control.
*/
this.attributionControl.setPrefix( '' );
/**
* @property {Kartographer.Box.ScaleControl} scaleControl Reference
* to scale control.
*/
this.scaleControl = new ScaleControl( { position: 'bottomright' } ).addTo( this );
if ( options.allowFullScreen ) {
// embed maps, and full screen is allowed
this.on( 'dblclick', function () {
// We need this hack to differentiate these events from `hashopen` events.
map.clicked = true;
mw.track( 'mediawiki.kartographer', {
action: 'open',
isFullScreen: true,
feature: map
} );
map.openFullScreen();
} );
/**
* @property {Kartographer.Box.OpenFullScreenControl|undefined} [openFullScreenControl=undefined]
* Reference to open full screen control.
*/
this.openFullScreenControl = new OpenFullScreenControl( { position: 'topright' } ).addTo( this );
}
/* Initialize map */
if ( !this._container.clientWidth || !this._container.clientHeight ) {
this._fixMapSize();
}
if ( !this.options.fullscreen ) {
this.doubleClickZoom.disable();
}
if ( !this.options.fullscreen && !options.alwaysInteractive ) {
this._invalidateInterative();
}
function ready() {
map.initView( options.center, options.zoom );
map.fire(
/**
* @event
* Fired when the Kartographer Map object is ready.
*/
'kartographerisready' );
mw.track( 'mediawiki.kartographer', {
action: 'initialize',
isFullScreen: !!map.options.fullscreen,
feature: map
} );
}
if ( this.parentMap ) {
$.each( this.parentMap.dataLayers, function ( groupId, layer ) {
map.addGeoJSONLayer( groupId, layer.getGeoJSON(), layer.options );
} );
ready();
return;
}
this.addDataGroups( options.dataGroups ).then( function () {
if ( typeof options.data === 'object' ) {
map.addDataLayer( options.data ).then( function () {
ready();
} );
} else {
ready();
}
} ).then( undefined, function ( err ) {
// console will catch this
throw err;
} );
},
// eslint-disable-next-line valid-jsdoc
/**
* Runs the given callback **when the Kartographer map has finished
* loading the data layers and positioning** the map with a center and
* zoom, **or immediately if it happened already**.
*
* @param {Function} callback
* @param {Object} [context]
* @chainable
*/
doWhenReady: function ( callback, context ) {
if ( this._kartographer_ready ) {
callback.call( context || this, this );
} else {
this.on( 'kartographerisready', callback, context );
}
return this;
},
// eslint-disable-next-line valid-jsdoc
/**
* Sets the initial center and zoom of the map, and optionally calls
* {@link #setView} to reposition the map.
*
* @param {L.LatLng|number[]} [center]
* @param {number} [zoom]
* @param {boolean} [setView=true]
* @chainable
*/
initView: function ( center, zoom, setView ) {
setView = setView !== false;
if ( Array.isArray( center ) ) {
if ( !isNaN( center[ 0 ] ) && !isNaN( center[ 1 ] ) ) {
center = L.latLng( center );
} else {
center = undefined;
}
}
zoom = isNaN( zoom ) ? undefined : zoom;
this._initialPosition = {
center: center,
zoom: zoom
};
if ( setView ) {
this.setView( center, zoom, null, true );
}
return this;
},
/**
* Gets and adds known data groups as layers onto the map.
*
* The data is loaded from the server if not found in memory.
*
* @param {string[]} dataGroups
* @return {jQuery.Promise}
*/
addDataGroups: function ( dataGroups ) {
var map = this,
deferred = $.Deferred();
if ( !dataGroups || !dataGroups.length ) {
return deferred.resolveWith().promise();
}
DataManager.loadGroups( dataGroups ).then( function ( dataGroups ) {
$.each( dataGroups, function ( key, group ) {
var layerOptions = {
attribution: group.attribution
};
if ( group.isExternal ) {
layerOptions.name = group.attribution;
}
if ( !$.isEmptyObject( group.getGeoJSON() ) ) {
map.addGeoJSONLayer( group.id, group.getGeoJSON(), layerOptions );
} else {
mw.log.warn( 'Layer not found or contains no data: "' + group.id + '"' );
}
} );
deferred.resolve();
} ).then( undefined, function ( err ) {
deferred.reject( err );
} );
return deferred.promise();
},
/**
* Creates a new GeoJSON layer and adds it to the map.
*
* @param {Object} groupData Features
* @param {Object} [options] Layer options
* @return {jQuery.Promise} Promise which resolves when the layer has been added
*/
addDataLayer: function ( groupData, options ) {
var map = this,
deferred = $.Deferred();
options = options || {};
DataManager.load( groupData ).then( function ( dataGroups ) {
$.each( dataGroups, function ( key, group ) {
var groupId = inlineDataLayerKey + inlineDataLayerId++,
layerOptions = {
attribution: group.attribution || options.attribution
};
if ( group.isExternal ) {
layerOptions.name = group.attribution;
}
if ( !$.isEmptyObject( group.getGeoJSON() ) ) {
map.addGeoJSONLayer( groupId, group.getGeoJSON(), layerOptions );
} else {
mw.log.warn( 'Layer not found or contains no data: "' + groupId + '"' );
}
} );
deferred.resolve();
} ).then( undefined, function ( err ) {
deferred.reject( err );
} );
return deferred.promise();
},
/**
* Creates a new GeoJSON layer and adds it to the map.
*
* @param {string} groupName The layer name (id without special
* characters or spaces).
* @param {Object} geoJson Features
* @param {Object} [options] Layer options
* @return {L.mapbox.FeatureLayer} Added layer
*/
addGeoJSONLayer: function ( groupName, geoJson, options ) {
var layer;
try {
layer = L.mapbox.featureLayer( geoJson, $.extend( {}, dataLayerOpts, options ) ).addTo( this );
layer.getAttribution = function () {
return this.options.attribution;
};
this.attributionControl.addAttribution( layer.getAttribution() );
this.dataLayers[ groupName ] = layer;
return layer;
} catch ( e ) {
mw.log( e );
}
},
/**
* Opens the map in a full screen dialog.
*
* **Uses Resource Loader module: {@link Kartographer.Dialog ext.kartographer.dialog}**
*
* @param {Object} [position] Map `center` and `zoom`.
*/
openFullScreen: function ( position ) {
this.doWhenReady( function () {
var map = this.options.link ? this : this.fullScreenMap;
position = position || this.getMapPosition();
if ( map && map._updatingHash ) {
// Skip - there is nothing to do.
map._updatingHash = false;
return;
} else if ( map ) {
this.doWhenReady( function () {
map.setView(
position.center,
position.zoom
);
} );
} else {
map = this.fullScreenMap = new Map( {
container: L.DomUtil.create( 'div', 'mw-kartographer-mapDialog-map' ),
center: position.center,
zoom: position.zoom,
featureType: this.featureType,
fullscreen: true,
captionText: this.captionText,
fullScreenRoute: this.fullScreenRoute,
parentMap: this
} );
// resets the right initial position silently afterwards.
map.initView(
this._initialPosition.center,
this._initialPosition.zoom,
false
);
}
mw.loader.using( 'ext.kartographer.dialog' ).done( function () {
map.doWhenReady( function () {
mw.loader.require( 'ext.kartographer.dialog' ).render( map );
} );
} );
}, this );
},
// eslint-disable-next-line
/**
* Closes full screen dialog.
*
* @chainable
*/
closeFullScreen: function () {
mw.loader.require( 'ext.kartographer.dialog' ).close();
return this;
},
/**
* Gets initial map center and zoom.
*
* @return {Object}
* @return {L.LatLng} return.center
* @return {number} return.zoom
*/
getInitialMapPosition: function () {
return this._initialPosition;
},
/**
* Gets current map center and zoom.
*
* @param {Object} [options]
* @param {boolean} [options.scaled=false] Whether you want the
* coordinates to be scaled to the current zoom.
* @return {Object}
* @return {L.LatLng} return.center
* @return {number} return.zoom
*/
getMapPosition: function ( options ) {
var center = this.getCenter(),
zoom = this.getZoom();
options = options || {};
if ( options.scaled ) {
center = L.latLng( this.getScaleLatLng( center.lat, center.lng, zoom ) );
}
return {
center: center,
zoom: zoom
};
},
/**
* Formats the full screen route of the map, such as:
* `/map/:maptagId(/:zoom/:longitude/:latitude)`
*
* The hash will contain the portion between parenthesis if and only if
* one of these 3 values differs from the initial setting.
*
* @return {string} The route to open the map in full screen mode.
*/
getHash: function () {
if ( !this._initialPosition ) {
return this.fullScreenRoute;
}
// eslint-disable-next-line vars-on-top
var hash = this.fullScreenRoute,
currentPosition = this.getMapPosition(),
initialPosition = this._initialPosition,
newHash = currentPosition.zoom + '/' + this.getScaleLatLng(
currentPosition.center.lat,
currentPosition.center.lng,
currentPosition.zoom
).join( '/' ),
initialHash = initialPosition.center && (
initialPosition.zoom + '/' +
this.getScaleLatLng(
initialPosition.center.lat,
initialPosition.center.lng,
initialPosition.zoom
).join( '/' )
);
if ( newHash !== initialHash ) {
hash += '/' + newHash;
}
return hash;
},
// eslint-disable-next-line valid-jsdoc
/**
* Sets the map at a certain zoom and position.
*
* When the zoom and map center are provided, it falls back to the
* original `L.Map#setView`.
*
* If the zoom or map center are not provided, this method will
* calculate some values so that all the point of interests fit within the
* map.
*
* **Note:** Unlike the original `L.Map#setView`, it accepts an optional
* fourth parameter to decide whether to update the container's data
* attribute with the calculated values (for performance).
*
* @param {L.LatLng|number[]|string} [center] Map center.
* @param {number} [zoom]
* @param {Object} [options] See [L.Map#setView](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-map-class/)
* documentation for the full list of options.
* @param {boolean} [save=false] Whether to update the data attributes.
* @chainable
*/
setView: function ( center, zoom, options, save ) {
var maxBounds,
initial = this.getInitialMapPosition();
if ( Array.isArray( center ) ) {
if ( !isNaN( center[ 0 ] ) && !isNaN( center[ 1 ] ) ) {
center = L.latLng( center );
} else {
center = undefined;
}
}
if ( center ) {
zoom = isNaN( zoom ) ? this.options.fallbackZoom : zoom;
L.Map.prototype.setView.call( this, center, zoom, options );
} else {
// Determines best center of the map
maxBounds = getValidBounds( this );
if ( maxBounds.isValid() ) {
this.fitBounds( maxBounds );
} else {
this.fitWorld();
}
// (Re-)Applies expected zoom
if ( initial && !isNaN( initial.zoom ) ) {
this.setZoom( initial.zoom );
} else if ( this.getZoom() > this.options.fallbackZoom ) {
this.setZoom( this.options.fallbackZoom );
}
if ( save ) {
// Updates map data.
this.initView( this.getCenter(), this.getZoom(), false );
// Updates container's data attributes to avoid `NaN` errors
if ( !this.fullscreen ) {
this.$container.closest( '.mw-kartographer-interactive' ).data( {
zoom: this.getZoom(),
longitude: this.getCenter().lng,
latitude: this.getCenter().lat
} );
}
}
}
return this;
},
/**
* Convenient method that formats the coordinates based on the zoom level.
*
* @param {number} lat
* @param {number} lng
* @param {number} [zoom]
* @return {Array} Array with the zoom (number), the latitude (string) and
* the longitude (string).
*/
getScaleLatLng: function ( lat, lng, zoom ) {
zoom = typeof zoom === 'undefined' ? this.getZoom() : zoom;
return [
lat.toFixed( precisionPerZoom[ zoom ] ),
lng.toFixed( precisionPerZoom[ zoom ] )
];
},
/**
* @localdoc Extended to also destroy the {@link #fullScreenMap} when
* it exists.
*
* @override
* @chainable
*/
remove: function () {
var parent = this.parentMap || this.parentLink;
if ( this.fullScreenMap ) {
L.Map.prototype.remove.call( this.fullScreenMap );
this.fullScreenMap = null;
}
if ( parent ) {
parent.fullScreenMap = null;
mw.track( 'mediawiki.kartographer', {
action: 'close',
isFullScreen: true,
feature: parent
} );
parent.clicked = false;
}
return L.Map.prototype.remove.call( this );
},
/**
* Fixes map size when the container is not visible yet, thus has no
* physical size.
*
* - In full screen, we take the viewport width and height.
* - Otherwise, the hack is to try jQuery which will pick up CSS
* dimensions. (T125263)
* - Finally, if the calculated size is still [0,0], the script looks
* for the first visible parent and takes its `height` and `width`
* to initialize the map.
*
* @protected
*/
_fixMapSize: function () {
var width, height, $visibleParent;
if ( this.options.fullscreen ) {
this._size = new L.Point(
window.innerWidth,
window.innerHeight
);
return;
}
$visibleParent = this.$container.closest( ':visible' );
// Try `max` properties.
width = $visibleParent.css( 'max-width' );
height = $visibleParent.css( 'max-height' );
width = ( !width || width === 'none' ) ? $visibleParent.width() : width;
height = ( !height || height === 'none' ) ? $visibleParent.height() : height;
while ( ( !height && $visibleParent.parent().length ) ) {
$visibleParent = $visibleParent.parent();
width = $visibleParent.outerWidth( true );
height = $visibleParent.outerHeight( true );
}
this._size = new L.Point( width, height );
},
// eslint-disable-next-line valid-jsdoc
/**
* Adds Leaflet.Sleep handler and overrides `invalidateSize` when the map
* is not in full screen mode.
*
* The new `invalidateSize` method calls {@link #toggleStaticState} to
* determine the new state and make the map either static or interactive.
*
* @chainable
* @protected
*/
_invalidateInterative: function () {
// add Leaflet.Sleep when the map isn't full screen.
this.addHandler( 'sleep', L.Map.Sleep );
// `invalidateSize` is triggered on window `resize` events.
this.invalidateSize = function ( options ) {
L.Map.prototype.invalidateSize.call( this, options );
if ( this.options.fullscreen ) {
// skip if the map is full screen
return this;
}
// Local debounce because oojs is not yet available.
if ( this._staticTimer ) {
clearTimeout( this._staticTimer );
}
this._staticTimer = setTimeout( this.toggleStaticState, 200 );
return this;
};
// Initialize static state.
this.toggleStaticState = L.Util.bind( this.toggleStaticState, this );
this.toggleStaticState();
return this;
},
// eslint-disable-next-line valid-jsdoc
/**
* Makes the map interactive IIF :
*
* - the `device width > 480px`,
* - there is at least a 200px horizontal margin.
*
* Otherwise makes it static.
*
* @chainable
*/
toggleStaticState: function () {
var deviceWidth = window.innerWidth,
// All maps static if deviceWitdh < 480px
isSmallWindow = deviceWidth <= 480,
staticMap;
// If the window is wide enough, make sure there is at least
// a 200px margin to scroll, otherwise make the map static.
staticMap = isSmallWindow || ( this.getSize().x + 200 ) > deviceWidth;
// Skip if the map is already static
if ( this._static === staticMap ) {
return;
}
// Toggle static/interactive state of the map
this._static = staticMap;
if ( staticMap ) {
this.sleep._sleepMap();
this.sleep.disable();
this.scrollWheelZoom.disable();
} else {
this.sleep.enable();
}
this.$container.toggleClass( 'mw-kartographer-static', staticMap );
return this;
}
} );
return Map;
}(
mediaWiki,
module.OpenFullScreenControl,
module.dataLayerOpts,
module.ScaleControl,
module.Data,
require( 'ext.kartographer.lib.topojson' ),
window
) );
module.map = ( function ( Map ) {
return function ( options ) {
return new Map( options );
};
}( module.Map ) );