JGirault has uploaded a new change for review.
https://gerrit.wikimedia.org/r/302286
Change subject: Refactor of the front end code to extend Mapbox.
......................................................................
Refactor of the front end code to extend Mapbox.
- Removed ext.kartographer.init => Its methods were reimplemented in local
files.
- Removed ext.kartographer.fullscreen => Replaced by ext.kartographer.dialog
- Removed ext.kartographer.live => Replaced by ext.kartographer.box
New APIs:
- [ext.kartographer.box] map() to create a Map. Works as a Leaflet/Mapbox
plugin.
- [ext.kartographer.box] link() to create a Link that opens a map in full
screen mode.
Change-Id: I00bbfcd15f608950f1110757a29dd1336f5a22e0
---
M extension.json
M includes/Tag/MapLink.php
M jsduck.json
A modules/box/Link.js
A modules/box/Map.js
A modules/box/closefullscreen_control.js
A modules/box/dataLayerOpts.js
R modules/box/enablePreview.js
A modules/box/index.js
A modules/box/openfullscreen_control.js
R modules/box/scale_control.js
A modules/dialog/dialog.js
A modules/dialog/index.js
D modules/fullscreen/CloseControl.js
D modules/fullscreen/MapDialog.js
D modules/fullscreen/index.js
D modules/fullscreen/indexRoute.js
D modules/kartographer.js
D modules/live/FullScreenControl.js
D modules/live/MWMap.js
D modules/live/dataLayerOpts.js
D modules/live/index.js
M modules/mapframe/mapframe.js
M modules/maplink/maplink.js
M modules/preview/preview.js
M modules/ve-maps/ve.ce.MWMapsNode.js
M modules/ve-maps/ve.ui.MWMapsDialog.js
27 files changed, 1,726 insertions(+), 1,414 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Kartographer
refs/changes/86/302286/1
diff --git a/extension.json b/extension.json
index a1e1bf3..b31e059 100644
--- a/extension.json
+++ b/extension.json
@@ -133,26 +133,55 @@
"desktop"
]
},
- "ext.kartographer.init": {
+ "ext.kartographer.link": {
"dependencies": [
- "ext.kartographer",
- "mediawiki.jqueryMsg"
+ "ext.kartographer.box",
+ "mediawiki.router"
],
"scripts": [
- "modules/kartographer.js"
+ "modules/maplink/maplink.js"
],
"targets": [
"mobile",
"desktop"
]
},
- "ext.kartographer.link": {
+ "ext.kartographer.box": {
"dependencies": [
- "ext.kartographer.init",
- "mediawiki.router"
+ "mapbox",
+ "ext.kartographer",
+ "ext.kartographer.style",
+ "ext.kartographer.settings",
+ "oojs-ui.styles.icons-media"
],
"scripts": [
- "modules/maplink/maplink.js"
+ "lib/leaflet.sleep.js",
+ "modules/box/closefullscreen_control.js",
+ "modules/box/openfullscreen_control.js",
+ "modules/box/scale_control.js",
+ "modules/box/dataLayerOpts.js",
+ "modules/box/map.js",
+ "modules/box/link.js",
+ "modules/box/enablePreview.js",
+ "modules/box/index.js"
+ ],
+ "messages": [
+ "kartographer-attribution"
+ ],
+ "targets": [
+ "mobile",
+ "desktop"
+ ]
+ },
+ "ext.kartographer.dialog": {
+ "dependencies": [
+ "ext.kartographer.box",
+ "mediawiki.router",
+ "oojs-ui-windows"
+ ],
+ "scripts": [
+ "modules/dialog/dialog.js",
+ "modules/dialog/index.js"
],
"targets": [
"mobile",
@@ -173,39 +202,11 @@
},
"ext.kartographer.frame": {
"dependencies": [
- "mapbox",
- "ext.kartographer.init",
- "mediawiki.router",
- "ext.kartographer.live"
+ "ext.kartographer.box",
+ "mediawiki.router"
],
"scripts": [
"modules/mapframe/mapframe.js"
- ],
- "targets": [
- "mobile",
- "desktop"
- ]
- },
- "ext.kartographer.live": {
- "dependencies": [
- "mapbox",
- "ext.kartographer.settings",
- "mediawiki.router",
- "oojs-ui.styles.icons-media",
- "ext.kartographer.init",
- "ext.kartographer.site"
- ],
- "scripts": [
- "lib/leaflet.sleep.js",
- "modules/live/ControlScale.js",
- "modules/live/FullScreenControl.js",
- "modules/live/dataLayerOpts.js",
- "modules/live/MWMap.js",
- "modules/live/enablePreview.js",
- "modules/live/index.js"
- ],
- "messages": [
- "kartographer-attribution"
],
"targets": [
"mobile",
@@ -224,29 +225,6 @@
"desktop"
]
},
- "ext.kartographer.fullscreen": {
- "dependencies": [
- "ext.kartographer.init",
- "ext.kartographer.site",
- "ext.kartographer.live",
- "mediawiki.router",
- "oojs-ui-windows"
- ],
- "scripts": [
- "modules/fullscreen/CloseControl.js",
- "modules/fullscreen/MapDialog.js",
- "modules/fullscreen/indexRoute.js",
- "modules/fullscreen/index.js"
- ],
- "messages": [
- "kartographer-fullscreen-close",
- "visualeditor-mwmapsdialog-title"
- ],
- "targets": [
- "mobile",
- "desktop"
- ]
- },
"ext.kartographer.editing": {
"scripts": [
"modules/editing/editing.js"
@@ -259,7 +237,7 @@
"ext.kartographer.editor": {
"dependencies": [
"leaflet.draw",
- "ext.kartographer.live"
+ "ext.kartographer.box"
],
"targets": [
"mobile",
@@ -290,7 +268,7 @@
"oojs-ui.styles.icons-location",
"ext.kartographer",
"ext.kartographer.init",
- "ext.kartographer.live",
+ "ext.kartographer.box",
"ext.kartographer.editing"
],
"targets": [
diff --git a/includes/Tag/MapLink.php b/includes/Tag/MapLink.php
index 65c9390..f11c667 100644
--- a/includes/Tag/MapLink.php
+++ b/includes/Tag/MapLink.php
@@ -32,7 +32,7 @@
$text = $this->parser->recursiveTagParse( $text, $this->frame );
$attrs = [
- 'class' => 'mw-kartographer-link',
+ 'class' => 'mw-kartographer-maplink',
'mw-data' => 'interface',
'data-style' => $this->mapStyle,
];
diff --git a/jsduck.json b/jsduck.json
index 2a54405..1105465 100644
--- a/jsduck.json
+++ b/jsduck.json
@@ -1,7 +1,7 @@
{
"--title": "Kartographer extension for Mediawiki - Documentation",
"--warnings": ["-nodoc(class,public)"],
- "--external":
"HTMLElement,jQuery,jQuery.Promise,jQuery.Promise.then,L.Control,L.Control.Layers,L.Control.Scale,L.Map,L.mapbox.FeatureLayer,L.Layer,L.GeoJSON,L.LatLng,L.LatLngBounds,OO.ui.Dialog,PruneCluster,PruneClusterForLeaflet",
+ "--external":
"HTMLElement,jQuery,jQuery.Promise,jQuery.Promise.then,L.Control,L.Control.Layers,L.Control.Scale,L.Control.Attribution,L.TileLayer,L.Map,L.mapbox.FeatureLayer,L.Layer,L.GeoJSON,L.LatLng,L.LatLngBounds,PruneCluster,PruneClusterForLeaflet,OO.ui.Dialog",
"--output": "docs/js",
"--": [
"modules/"
diff --git a/modules/box/Link.js b/modules/box/Link.js
new file mode 100644
index 0000000..a20b3f8
--- /dev/null
+++ b/modules/box/Link.js
@@ -0,0 +1,112 @@
+/* globals module */
+/**
+ * # Kartographer Link class
+ *
+ * Binds a `click` event to a `container`, that creates
+ * {@link Kartographer.Box.MapClass a map with layers, markers, and
+ * interactivity}, and opens it in a full screen dialog.
+ *
+ * @alias KartographerLink
+ * @class Kartographer.Box.LinkClass
+ */
+module.Link = ( function ( $ ) {
+
+ var Link;
+
+ /*jscs:disable disallowDanglingUnderscores */
+ /**
+ * @constructor
+ * @param {Object} options **Configuration and options:**
+ * @param {HTMLElement} options.container **Link container.**
+ * @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 {Array|L.LatLng} [options.center] **Initial map center.**
+ * @param {number} [options.zoom] **Initial map zoom.**
+ * @param {string} [options.fullScreenRoute] Route associated to this
map
+ * _(internal, used by "`<maplink>`")_.
+ * @member Kartographer.Box.LinkClass
+ */
+ Link = function ( options ) {
+ /**
+ * Reference to the link container.
+ *
+ * @type {HTMLElement}
+ */
+ this.container = options.container;
+
+ /**
+ * Reference to the map container as a jQuery element.
+ *
+ * @type {jQuery}
+ */
+ this.$container = $( this.container );
+ this.$container.addClass( 'mw-kartographer-link' );
+
+ this.center = options.center || 'auto';
+ this.zoom = options.zoom || 'auto';
+
+ this.opened = false;
+
+ this.useRouter = !!options.fullScreenRoute;
+ this.fullScreenRoute = options.fullScreenRoute || null;
+ this.dataGroups = options.dataGroups;
+ this.data = options.data;
+ /**
+ * @property {Kartographer.Box.MapClass} [fullScreenMap=null]
Reference
+ * to the associated full screen map.
+ * @protected
+ */
+ this.fullScreenMap = null;
+
+ if ( this.useRouter && this.container.tagName === 'A' ) {
+ this.container.href = '#' + this.fullScreenRoute;
+ } else {
+ this.$container.on( 'click', L.Util.bind( function () {
+ this.openFullScreen();
+ }, this ) );
+ }
+ };
+
+ /**
+ * Opens the map associated to the link in a full screen dialog.
+ *
+ * **Uses Resource Loader module: {@link Kartographer.Dialog
ext.kartographer.dialog}**
+ *
+ * @param {Object} [position] Map `center` and `zoom`.
+ * @member Kartographer.Box.LinkClass
+ */
+ Link.prototype.openFullScreen = function ( position ) {
+
+ var map = this.map;
+
+ position = position || { center: this.center, zoom: this.zoom };
+
+ if ( this.fullScreenMap &&
this.fullScreenMap._container._leaflet ) {
+ map = this.fullScreenMap;
+
+ map.setView(
+ position.center,
+ position.zoom
+ );
+ } else {
+ map = this.fullScreenMap = L.kartographer.map( {
+ container: document.createElement( 'div' ),
+ fullscreen: true,
+ link: true,
+ center: position.center,
+ zoom: position.zoom,
+ dataGroups: this.dataGroups,
+ data: this.data,
+ fullScreenRoute: this.fullScreenRoute
+ } );
+ }
+
+ mw.loader.using( 'ext.kartographer.dialog' ).done( function () {
+ mw.loader.require( 'ext.kartographer.dialog' ).render(
map );
+ } );
+ };
+
+ return Link;
+} )( jQuery );
diff --git a/modules/box/Map.js b/modules/box/Map.js
new file mode 100644
index 0000000..729eb11
--- /dev/null
+++ b/modules/box/Map.js
@@ -0,0 +1,838 @@
+/* globals module */
+/**
+ * # 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, CloseFullScreenControl,
dataLayerOpts, ScaleControl, document, undefined ) {
+
+ var scale, urlFormat,
+ mapServer = mw.config.get( 'wgKartographerMapServer' ),
+ worldLatLng = new L.LatLngBounds( [ -90, -180 ], [ 90, 180 ] ),
+ Map,
+ isMobile = mw.config.get( 'skin' ) === 'minerva';
+
+ 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: 1000,
+ sleepNote: false,
+ sleepOpacity: 1,
+ // the default zoom applied when `longitude` and `latitude` were
+ // specified, but zoom was not.å
+ fallbackZoom: 13
+ } );
+
+ /**
+ * Convenient method that formats the coordinates based on the zoom
level.
+ *
+ * @param {number} zoom
+ * @param {number} lat
+ * @param {number} lng
+ * @return {Array} Array with the zoom (number), the latitude (string)
and
+ * the longitude (string).
+ */
+ function getScaleCoords( zoom, lat, lng ) {
+ var precisionPerZoom = [ 0, 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4,
4, 4, 4, 4, 5, 5 ];
+
+ return [
+ zoom,
+ lat.toFixed( precisionPerZoom[ zoom ] ),
+ lng.toFixed( precisionPerZoom[ zoom ] )
+ ];
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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`
+ *
+ * @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;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the data for the list of groups.
+ *
+ * If the data is not already loaded (`wgKartographerLiveData`), an
+ * asynchronous request will be made to fetch the missing groups.
+ * The new data is then added to `wgKartographerLiveData`.
+ *
+ * @param {string[]} dataGroups Data group names.
+ * @return {jQuery.Promise} Promise which resolves with the group data,
+ * an object keyed by group name
+ * @private
+ */
+ function getMapGroupData( dataGroups ) {
+ var deferred = $.Deferred(),
+ groupsLoaded = mw.config.get( 'wgKartographerLiveData'
),
+ groupsToLoad = [],
+ promises = [];
+
+ if ( !groupsLoaded ) {
+ // Keep the reference to groupsLoaded, as it shouldn't
change again
+ groupsLoaded = {};
+ mw.config.set( 'wgKartographerLiveData', groupsLoaded );
+ }
+
+ // For each requested layer, make sure it is loaded or is
promised to be loaded
+ $( dataGroups ).each( function ( key, value ) {
+ var data = groupsLoaded[ value ];
+ if ( data === undefined ) {
+ groupsToLoad.push( value );
+ // Once loaded, this value will be replaced
with the received data
+ groupsLoaded[ value ] = deferred.promise();
+ } else if ( data !== null && $.isFunction( data.then )
) {
+ promises.push( data );
+ }
+ } );
+
+ if ( groupsToLoad.length ) {
+ promises.push( deferred.promise() );
+ }
+ if ( !promises.length ) {
+ return deferred.resolve( groupsLoaded ).promise();
+ }
+
+ new mw.Api().get( {
+ action: 'query',
+ formatversion: '2',
+ titles: mw.config.get( 'wgPageName' ),
+ prop: 'mapdata',
+ mpdgroups: groupsToLoad.join( '|' )
+ } ).done( function ( data ) {
+ var rawMapData = data.query.pages[ 0 ].mapdata,
+ mapData = rawMapData && JSON.parse( rawMapData
) || {};
+ $.extend( groupsLoaded, mapData );
+ deferred.resolve( groupsLoaded );
+ } );
+
+ return $.when.apply( $, promises ).then( function () {
+ // All pending promises are done
+ return groupsLoaded;
+ } ).promise();
+ }
+
+ /*jscs:disable disallowDanglingUnderscores */
+ 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 {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';
+
+ if ( options.center === 'auto' ) {
+ options.center = undefined;
+ }
+ if ( options.zoom === 'auto' ) {
+ options.zoom = undefined;
+ }
+
+ if ( isMobile && !options.fullscreen ) {
+ options.container =
this._responsiveContainerWrap( options.container );
+ }
+
+ $( options.container ).addClass( 'mw-kartographer-map'
);
+
+ 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 () {
+ /*jscs:disable
requireCamelCaseOrUpperCaseIdentifiers*/
+ this._kartographer_ready = true;
+ /*jscs:enable
requireCamelCaseOrUpperCaseIdentifiers*/
+ }, this );
+
+ /**
+ * @property {Kartographer.Box.MapClass}
[parentMap=null] Reference
+ * to the parent map.
+ * @protected
+ */
+ this.parentMap = options.parentMap || null;
+
+ /**
+ * @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 {Array} 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 () {
+ this.openFullScreen();
+ }, this );
+
+ /**
+ * @property
{Kartographer.Box.OpenFullScreenControl|undefined}
[openFullScreenControl=undefined]
+ * Reference to open full screen control.
+ */
+ this.openFullScreenControl = new
OpenFullScreenControl( { position: 'topright' } ).addTo( this );
+ } else if ( options.fullscreen ) {
+ // full screen maps
+ /**
+ * @property
{Kartographer.Box.CloseFullScreenControl|undefined}
[closeFullScreenControl=undefined]
+ * Reference to close full screen control.
+ */
+ this.closeFullScreenControl = new
CloseFullScreenControl( { position: 'topright' } ).addTo( this );
+ }
+
+ /* Initialize map */
+
+ if ( !this._container.clientWidth ||
!this._container.clientHeight ) {
+ this._fixMapSize();
+ }
+ this.doubleClickZoom.disable();
+
+ if ( !this.options.fullscreen ) {
+ this._invalidateInterative();
+ }
+
+ this.addDataGroups( options.dataGroups ).then(
L.Util.bind( function () {
+ if ( typeof options.data === 'object' ) {
+ this.addDataLayer( options.data );
+ }
+
+ this.initView( options.center, options.zoom );
+ this.fire(
+ /**
+ * @event
+ * Fired when the Kartographer Map
object is ready.
+ */
+ 'kartographerisready' );
+ }, this ) );
+ },
+
+ /**
+ * 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 ) {
+ /*jscs:disable requireCamelCaseOrUpperCaseIdentifiers*/
+ if ( this._kartographer_ready ) {
+ callback.call( context || this, this );
+ } else {
+ this.on( 'kartographerisready', callback,
context );
+ }
+ /*jscs:enable requireCamelCaseOrUpperCaseIdentifiers*/
+ return this;
+ },
+
+ /**
+ * Sets the initial center and zoom of the map, and optionally
calls
+ * {@link #setView} to reposition the map.
+ *
+ * @param {L.LatLng|Array} [center]
+ * @param {number} [zoom]
+ * @param {boolean} [setView=false]
+ * @chainable
+ */
+ initView: function ( center, zoom, setView ) {
+ setView = setView === false ? false : true;
+
+ if ( center ) {
+ center = L.latLng( center );
+ }
+ 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 ) {
+ return deferred.resolveWith().promise();
+ }
+ getMapGroupData( dataGroups ).then( function ( mapData
) {
+ $.each( dataGroups, function ( index, group ) {
+ if ( !$.isEmptyObject( mapData[ group ]
) ) {
+ map.addDataLayer( group,
mapData[ group ] );
+ } else {
+ mw.log.warn( 'Layer not found
or contains no data: "' + group + '"' );
+ }
+ } );
+ deferred.resolveWith().promise();
+ } );
+ 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
+ */
+ addDataLayer: function ( groupName, geoJson ) {
+ var layer;
+ try {
+ layer = L.mapbox.featureLayer( geoJson,
dataLayerOpts ).addTo( this );
+ 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._initialPosition;
+
+ 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,
+ fullscreen: true,
+ dataGroups:
this.options.dataGroups,
+ 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 );
+ },
+
+ /**
+ * 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.
+ *
+ * @return {Object}
+ * @return {L.LatLng} return.center
+ * @return {number} return.zoom
+ */
+ getMapPosition: function () {
+ var center = this.getCenter();
+ return {
+ center: center,
+ zoom: this.getZoom()
+ };
+ },
+
+ /**
+ * 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 () {
+ /*jscs:disable requireVarDeclFirst*/
+ if ( !this._initialPosition ) {
+ return this.fullScreenRoute;
+ }
+
+ var hash = this.fullScreenRoute,
+ currentPosition = this.getMapPosition(),
+ newHash = getScaleCoords(
+ currentPosition.zoom,
+ currentPosition.center.lat,
+ currentPosition.center.lng
+ ).join( '/' ),
+ initialHash = this._initialPosition.center &&
getScaleCoords(
+ this._initialPosition.zoom,
+
this._initialPosition.center.lat,
+ this._initialPosition.center.lng
+ ).join( '/' );
+
+ if ( newHash !== initialHash ) {
+ hash += '/' + newHash;
+ }
+
+ /*jscs:enable requireVarDeclFirst*/
+ return hash;
+ },
+
+ /**
+ * 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|Array|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 ( center ) {
+ center = L.latLng( 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 && initial.zoom ) {
+ this.setZoom( initial.zoom );
+ }
+
+ 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;
+ },
+
+ /**
+ * @localdoc Extended to also destroy the {@link
#fullScreenMap} when
+ * it exists.
+ *
+ * @override
+ * @chainable
+ */
+ remove: function () {
+ if ( this.fullScreenMap ) {
+ L.Map.prototype.remove.call( this.fullScreenMap
);
+ this.fullScreenMap = null;
+ }
+ if ( this.parentMap ) {
+ this.parentMap.fullScreenMap = null;
+ }
+
+ 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(
+ document.body.clientWidth,
+ document.body.clientHeight
+ );
+ return;
+ }
+
+ $visibleParent = this.$container.closest( ':visible' );
+
+ // Get `max` properties in case the container was
wrapped
+ // with {@link #responsiveContainerWrap}.
+ 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 );
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * Wraps a map container to make it (and its map) responsive on
+ * mobile (MobileFrontend).
+ *
+ * The initial `mapContainer`:
+ *
+ * <div class="mw-kartographer-interactive" style="height:
Y; width: X;">
+ * <!-- this is the component carrying Leaflet.Map -->
+ * </div>
+ *
+ * Becomes :
+ *
+ * <div class="mw-kartographer-interactive
mw-kartographer-responsive" style="max-height: Y; max-width: X;">
+ * <div class="mw-kartographer-responder"
style="padding-bottom: (100*Y/X)%">
+ * <div>
+ * <!-- this is the component carrying
Leaflet.Map -->
+ * </div>
+ * </div>
+ * </div>
+ *
+ * **Note:** the container that carries the map data remains
the initial
+ * `mapContainer` passed in arguments. Its selector remains
`.mw-kartographer-interactive`.
+ * However it is now a sub-child that carries the map.
+ *
+ * **Note 2:** the CSS applied to these elements vary whether
the map width
+ * is absolute (px) or relative (%). The example above
describes the absolute
+ * width case.
+ *
+ * @param {HTMLElement} mapContainer Initial component to carry
the map.
+ * @return {HTMLElement} New map container to carry the map.
+ * @protected
+ */
+ _responsiveContainerWrap: function ( mapContainer ) {
+ var $container = $( mapContainer ),
+ $responder, map,
+ width = mapContainer.style.width,
+ isRelativeWidth = width.slice( -1 ) === '%',
+ height = +( mapContainer.style.height.slice( 0,
-2 ) ),
+ containerCss, responderCss;
+
+ // Convert the value to a string.
+ width = isRelativeWidth ? width : +( width.slice( 0, -2
) );
+
+ if ( isRelativeWidth ) {
+ containerCss = {};
+ responderCss = {
+ // The inner container must occupy the
full height
+ height: height
+ };
+ } else {
+ containerCss = {
+ // Remove explicitly set dimensions
+ width: '',
+ height: '',
+ // Prevent over-sizing
+ 'max-width': width,
+ 'max-height': height
+ };
+ responderCss = {
+ // Use padding-bottom trick to maintain
original aspect ratio
+ 'padding-bottom': ( 100 * height /
width ) + '%'
+ };
+ }
+ $container.addClass( 'mw-kartographer-responsive'
).css( containerCss );
+ $responder = $( '<div>' ).addClass(
'mw-kartographer-responder' ).css( responderCss );
+
+ map = document.createElement( 'div' );
+ this.$outerContainer = $container.append(
$responder.append( map ) );
+ return map;
+ }
+ } );
+
+ return Map;
+} )(
+ mediaWiki,
+ module.OpenFullScreenControl,
+ module.CloseFullScreenControl,
+ module.dataLayerOpts,
+ module.ScaleControl,
+ document
+);
+
+module.map = ( function ( Map ) {
+ return function ( options ) {
+ return new Map( options );
+ };
+} )( module.Map );
diff --git a/modules/box/closefullscreen_control.js
b/modules/box/closefullscreen_control.js
new file mode 100644
index 0000000..98636ce
--- /dev/null
+++ b/modules/box/closefullscreen_control.js
@@ -0,0 +1,46 @@
+/* globals module */
+/*jscs:disable disallowDanglingUnderscores */
+/**
+ * # Control to close the full screen dialog.
+ *
+ * See [L.Control](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-control/)
+ * documentation for more details.
+ *
+ * @class Kartographer.Box.CloseFullScreenControl
+ * @extends L.Control
+ */
+module.CloseFullScreenControl = L.Control.extend( {
+ options: {
+ position: 'topright'
+ },
+
+ /**
+ * Creates the control element.
+ *
+ * @override
+ * @protected
+ */
+ onAdd: function () {
+ var container = L.DomUtil.create( 'div', 'leaflet-bar' ),
+ link = L.DomUtil.create( 'a', 'oo-ui-icon-close',
container );
+
+ link.href = '';
+ link.title = mw.msg( 'kartographer-fullscreen-close' );
+
+ L.DomEvent.addListener( link, 'click', this.closeFullScreen,
this );
+ L.DomEvent.disableClickPropagation( container );
+
+ return container;
+ },
+
+ /**
+ * Closes the full screen dialog on `click`.
+ *
+ * @param {Event} e
+ * @protected
+ */
+ closeFullScreen: function ( e ) {
+ L.DomEvent.stop( e );
+ this._map.closeFullScreen();
+ }
+} );
diff --git a/modules/box/dataLayerOpts.js b/modules/box/dataLayerOpts.js
new file mode 100644
index 0000000..51fcb57
--- /dev/null
+++ b/modules/box/dataLayerOpts.js
@@ -0,0 +1,30 @@
+/* globals module */
+/**
+ * # Options passed to Mapbox when adding a feature layer.
+ *
+ * `L.mapbox.featureLayer` provides an easy way to add a layer from GeoJSON
+ * into your map. This module is the set of options passed to this method
+ * when it is called.
+ *
+ * See
[L.mapbox.featureLayer](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-mapbox-featurelayer/)
+ * documentation for the full list of options.
+ *
+ * @class Kartographer.Box.dataLayerOpts
+ * @singleton
+ * @private
+ */
+module.dataLayerOpts = {
+ /**
+ * A function that accepts a string containing tooltip data,
+ * and returns a sanitized result for HTML display.
+ *
+ * The default Mapbox sanitizer is disabled because GeoJSON has already
+ * passed through Kartographer's internal sanitizer (avoids double
+ * sanitization).
+ *
+ * @param {Object} geojson
+ */
+ sanitizer: function ( geojson ) {
+ return geojson;
+ }
+};
diff --git a/modules/live/enablePreview.js b/modules/box/enablePreview.js
similarity index 83%
rename from modules/live/enablePreview.js
rename to modules/box/enablePreview.js
index afcba13..4e984b9 100644
--- a/modules/live/enablePreview.js
+++ b/modules/box/enablePreview.js
@@ -1,10 +1,12 @@
/* globals module */
/**
+ * # Preview mode
+ *
* Module executing code to load {@link Kartographer.Preview
ext.kartographer.preview}
* when it detects preview edit mode.
*
- * @alias enablePreview
- * @class Kartographer.Live.enablePreview
+ * @class Kartographer.Box.enablePreview
+ * @singleton
* @private
*/
module.enablePreview = ( function ( $, mw ) {
diff --git a/modules/box/index.js b/modules/box/index.js
new file mode 100644
index 0000000..fc9b582
--- /dev/null
+++ b/modules/box/index.js
@@ -0,0 +1,66 @@
+/* globals module */
+/**
+ * **Resource Loader module: {@link Kartographer.Box ext.kartographer.box}**
+ *
+ * @alias ext.kartographer.box
+ * @class Kartographer.Box
+ * @singleton
+ */
+L.kartographer = module.exports = {
+ /**
+ * @type {Kartographer.Box.OpenFullScreenControl}
+ * @ignore
+ */
+ OpenFullScreenControl: module.OpenFullScreenControl,
+
+ /**
+ * @type {Kartographer.Box.CloseFullScreenControl}
+ * @ignore
+ */
+ CloseFullScreenControl: module.CloseFullScreenControl,
+
+ /**
+ * @type {Kartographer.Box.ScaleControl}
+ * @ignore
+ */
+ ScaleControl: module.ScaleControl,
+
+ /**
+ * @type {Kartographer.Box.MWMap}
+ * @ignore
+ */
+ Map: module.Map,
+
+ /**
+ * Use this method to create a {@link Kartographer.Box.MapClass Map}
+ * object.
+ *
+ * See {@link Kartographer.Box.MapClass#constructor} for the list of
options.
+ *
+ * @return {Kartographer.Box.MapClass}
+ * @member Kartographer.Box
+ */
+ map: function ( options ) {
+ var Map = this.Map;
+ return new Map( options );
+ },
+
+ /**
+ * @type {Kartographer.Box.LinkClass}
+ * @ignore
+ */
+ Link: module.Link,
+
+ /**
+ * Use this method to create a {@link Kartographer.Box.LinkClass Link}
+ * object.
+ *
+ * See {@link Kartographer.Box.LinkClass#constructor} for the list of
options.
+ *
+ * @return {Kartographer.Box.LinkClass}
+ */
+ link: function ( options ) {
+ var Link = this.Link;
+ return new Link( options );
+ }
+};
diff --git a/modules/box/openfullscreen_control.js
b/modules/box/openfullscreen_control.js
new file mode 100644
index 0000000..d712b4f
--- /dev/null
+++ b/modules/box/openfullscreen_control.js
@@ -0,0 +1,72 @@
+/* globals module */
+/*jscs:disable disallowDanglingUnderscores */
+/**
+ * # Control to open the map in a full screen dialog.
+ *
+ * See [L.Control](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-control/)
+ * documentation for more details.
+ *
+ * @class Kartographer.Box.OpenFullScreenControl
+ * @extends L.Control
+ */
+module.OpenFullScreenControl = L.Control.extend( {
+ options: {
+ // Do not switch for RTL because zoom also stays in place
+ position: 'topright'
+ },
+
+ /**
+ * Creates the control element.
+ *
+ * @override
+ * @protected
+ */
+ onAdd: function () {
+ var container = L.DomUtil.create( 'div', 'leaflet-bar
leaflet-control-static' );
+
+ this.link = L.DomUtil.create( 'a', 'oo-ui-icon-fullScreen',
container );
+ this.link.title = mw.msg( 'kartographer-fullscreen-text' );
+
+ if ( this._map.useRouter ) {
+ this.updateHash();
+ this._map.on( 'moveend', this.onMapMove, this );
+ } else {
+ // the router will handle it otherwise
+ L.DomEvent.addListener( this.link, 'click',
this.openFullScreen, this );
+ }
+ L.DomEvent.disableClickPropagation( container );
+
+ return container;
+ },
+
+ /**
+ * Updates the hash on `moveend`.
+ *
+ * @override
+ * @protected
+ */
+ onMapMove: function () {
+ if ( !this._map._loaded ) {
+ return false;
+ }
+ this.updateHash();
+ },
+
+ /**
+ * Updates the link with the latest map hash.
+ */
+ updateHash: function () {
+ this.link.href = '#' + this._map.getHash();
+ },
+
+ /**
+ * Opens the full screen dialog on `click`.
+ *
+ * @param {Event} e
+ * @protected
+ */
+ openFullScreen: function ( e ) {
+ L.DomEvent.stop( e );
+ this._map.openFullScreen();
+ }
+} );
diff --git a/modules/live/ControlScale.js b/modules/box/scale_control.js
similarity index 81%
rename from modules/live/ControlScale.js
rename to modules/box/scale_control.js
index 78fe071..c793476 100644
--- a/modules/live/ControlScale.js
+++ b/modules/box/scale_control.js
@@ -1,16 +1,15 @@
/* globals module */
/*jscs:disable disallowDanglingUnderscores, requireVarDeclFirst */
/**
- * Control to display the scale.
+ * # Control to display the scale.
*
- * See
[L.Control.Scale](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-control-scale/)
+ * See [L.Control](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-control/)
* documentation for more details.
*
- * @alias ControlScale
- * @class Kartographer.Live.ControlScale
- * @extends L.Control.Scale
+ * @class Kartographer.Box.ScaleControl
+ * @extends L.Control
*/
-module.ControlScale = L.Control.Scale.extend( {
+module.ScaleControl = L.Control.Scale.extend( {
isMetric: true,
diff --git a/modules/dialog/dialog.js b/modules/dialog/dialog.js
new file mode 100644
index 0000000..9b2cdde
--- /dev/null
+++ b/modules/dialog/dialog.js
@@ -0,0 +1,134 @@
+/* globals module, require */
+/**
+ * Dialog for displaying maps in full screen mode.
+ *
+ * See
[OO.ui.Dialog](https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.Dialog)
+ * documentation for more details.
+ *
+ * @class Kartographer.Dialog.DialogClass
+ * @extends OO.ui.Dialog
+ */
+module.Dialog = ( function ( $, mw, kartobox, router ) {
+
+ /**
+ * @constructor
+ * @type {Kartographer.Dialog.DialogClass}
+ */
+ var MapDialog = function () {
+ // Parent method
+ MapDialog.super.apply( this, arguments );
+ };
+
+ /* Inheritance */
+
+ OO.inheritClass( MapDialog, OO.ui.Dialog );
+
+ /* Static Properties */
+
+ MapDialog.static.size = 'full';
+
+ /* Methods */
+
+ MapDialog.prototype.initialize = function () {
+ // Parent method
+ MapDialog.super.prototype.initialize.apply( this, arguments );
+
+ this.map = null;
+ };
+
+ MapDialog.prototype.getActionProcess = function ( action ) {
+ var dialog = this;
+
+ if ( !action ) {
+ return new OO.ui.Process( function () {
+ dialog.map.closeFullScreen();
+ } );
+ }
+ return MapDialog.super.prototype.getActionProcess.call( this,
action );
+ };
+
+ /**
+ * Tells the router to navigate to the current full screen map route.
+ */
+ MapDialog.prototype.updateHash = function () {
+ var hash = this.map.getHash();
+
+ // Avoid extra operations
+ if ( this.lastHash !== hash ) {
+ /*jscs:disable disallowDanglingUnderscores */
+ this.map._updatingHash = true;
+ /*jscs:enable disallowDanglingUnderscores */
+ router.navigate( hash );
+ this.lastHash = hash;
+ }
+ };
+
+ /**
+ * Listens to `moveend` event and calls {@link #updateHash}.
+ *
+ * This method is throttled, meaning the method will be called at most
once per
+ * every 250 milliseconds.
+ */
+ MapDialog.prototype.onMapMove = OO.ui.throttle( function () {
+ // Stop listening to `moveend` event while we're
+ // manually moving the map (updating from a hash),
+ // or if the map is not yet loaded.
+ /*jscs:disable disallowDanglingUnderscores */
+ if ( this.movingMap || !this.map || !this.map._loaded ) {
+ return false;
+ }
+ /*jscs:enable disallowDanglingUnderscores */
+ this.updateHash();
+ }, 250 );
+
+ MapDialog.prototype.getSetupProcess = function ( options ) {
+ return MapDialog.super.prototype.getSetupProcess.call( this,
options )
+ .next( function () {
+
+ if ( options.map !== this.map ) {
+
+ if ( this.map ) {
+ this.map.remove();
+ }
+
+ this.map = options.map;
+ this.$body.empty().append(
+ this.map.$container.css(
'position', '' )
+ );
+ }
+ }, this );
+ };
+
+ MapDialog.prototype.getReadyProcess = function ( data ) {
+ return MapDialog.super.prototype.getReadyProcess.call( this,
data )
+ .next( function () {
+
+ this.map.doWhenReady( function ( ) {
+
+ if ( this.map.useRouter ) {
+ this.map.on( 'moveend',
this.onMapMove, this );
+ }
+
+ mw.hook( 'wikipage.maps' ).fire(
this.map, true /* isFullScreen */ );
+ }, this );
+ }, this );
+ };
+
+ MapDialog.prototype.getTeardownProcess = function ( data ) {
+ return MapDialog.super.prototype.getTeardownProcess.call( this,
data )
+ .next( function () {
+ this.map.remove();
+ this.map = null;
+ }, this );
+ };
+
+ return function () {
+ return new MapDialog();
+ };
+
+} )(
+ jQuery,
+ mediaWiki,
+ require( 'ext.kartographer.box' ),
+ require( 'mediawiki.router' )
+);
diff --git a/modules/dialog/index.js b/modules/dialog/index.js
new file mode 100644
index 0000000..16c1366
--- /dev/null
+++ b/modules/dialog/index.js
@@ -0,0 +1,92 @@
+/* globals module, require */
+/**
+ * Module to help rendering maps in a full screen dialog.
+ *
+ * @alias ext.kartographer.dialog
+ * @class Kartographer.Dialog
+ * @singleton
+ */
+module.exports = ( function ( Dialog, router ) {
+
+ var windowManager, mapDialog, routerEnabled;
+
+ function getMapDialog() {
+ mapDialog = mapDialog || new Dialog();
+ return mapDialog;
+ }
+
+ function getWindowManager() {
+ if ( !windowManager ) {
+ windowManager = new OO.ui.WindowManager();
+ $( 'body' ).append( windowManager.$element );
+ getWindowManager().addWindows( [ getMapDialog() ] );
+ }
+ return windowManager;
+ }
+
+ function close() {
+ if ( mapDialog ) {
+ mapDialog.close();
+ }
+ mapDialog = null;
+ windowManager = null;
+ }
+
+ return {
+ /**
+ * Opens the map dialog and renders the map.
+ *
+ * @param {Kartographer.Box.MapClass} map
+ */
+ render: function ( map ) {
+
+ var window = getWindowManager(),
+ dialog = getMapDialog();
+
+ if ( map.useRouter && !routerEnabled ) {
+ router.route( '', function () {
+ close();
+ } );
+ }
+
+ if ( !window.opened ) {
+ getWindowManager()
+ .openWindow( dialog, { map: map } )
+ .then( function ( opened ) {
+ // It takes 250ms for the
dialog to open,
+ // we'd better invalidate the
size once it opened.
+ // setTimeout( function () {
+ // map.invalidateSize();
+ // }, 300 );
+ return opened;
+ } )
+ .then( function ( closing ) {
+ if ( map.parentMap ) {
+ map.parentMap.setView(
+ map.getCenter(),
+ map.getZoom()
+ );
+ }
+ dialog.close();
+ mapDialog = null;
+ windowManager = null;
+ return closing;
+ } );
+ } else if ( dialog.map !== map ) {
+ dialog.setup.call( dialog, { map: map } );
+ dialog.ready.call( dialog, { map: map } );
+ }
+ },
+
+ /**
+ * Closes the map dialog.
+ */
+ close: function () {
+ if ( mapDialog && mapDialog.map.useRouter ) {
+ router.navigate( '' );
+ } else {
+ close();
+ }
+ }
+ };
+} )( module.Dialog, require( 'mediawiki.router' ) );
diff --git a/modules/fullscreen/CloseControl.js
b/modules/fullscreen/CloseControl.js
deleted file mode 100644
index 3feda5a..0000000
--- a/modules/fullscreen/CloseControl.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/* globals module */
-/**
- * Control to close the full screen dialog.
- *
- * See [L.Control](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-control/)
- * documentation for more details.
- *
- * @alias FullScreenCloseControl
- * @class Kartographer.Fullscreen.CloseControl
- * @extends L.Control
- */
-module.FullScreenCloseControl = L.Control.extend( {
- options: {
- position: 'topright'
- },
-
- onAdd: function () {
- var container = L.DomUtil.create( 'div', 'leaflet-bar' ),
- link = L.DomUtil.create( 'a', 'oo-ui-icon-close',
container );
-
- this.href = '#';
- link.title = mw.msg( 'kartographer-fullscreen-close' );
-
- L.DomEvent.addListener( link, 'click', this.onClick, this );
- L.DomEvent.disableClickPropagation( container );
-
- return container;
- },
-
- onClick: function ( e ) {
- L.DomEvent.stop( e );
-
- this.options.dialog.executeAction( '' );
- }
-} );
diff --git a/modules/fullscreen/MapDialog.js b/modules/fullscreen/MapDialog.js
deleted file mode 100644
index 5901abe..0000000
--- a/modules/fullscreen/MapDialog.js
+++ /dev/null
@@ -1,198 +0,0 @@
-/* globals module, require */
-/**
- * Dialog for displaying maps in full screen mode.
- *
- * See
[OO.ui.Dialog](https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.Dialog)
- * documentation for more details.
- *
- * @alias MapDialog
- * @class Kartographer.Fullscreen.MapDialog
- * @extends OO.ui.Dialog
- */
-module.MapDialog = ( function ( $, mw, kartographer, kartoLive, router,
CloseControl ) {
-
- /**
- * @constructor
- * @type {Kartographer.Fullscreen.MapDialog}
- */
- var MapDialog = function () {
- // Parent method
- MapDialog.super.apply( this, arguments );
- };
-
- /* Inheritance */
-
- OO.inheritClass( MapDialog, OO.ui.Dialog );
-
- /* Static Properties */
-
- MapDialog.static.size = 'full';
-
- /* Methods */
-
- MapDialog.prototype.initialize = function () {
- // Parent method
- MapDialog.super.prototype.initialize.apply( this, arguments );
-
- this.map = null;
- this.mapData = null;
- this.$map = null;
- };
-
- /**
- * Changes the map within the map dialog.
- *
- * If the new map is the same at the previous map, we reuse the same map
- * object and simply update the zoom and the center of the map.
- *
- * If the new map is different, we keep the dialog open and simply
- * replace the map object with a new one.
- *
- * @param {Object} mapData The data for the new map.
- * @param {Object} [mapData.fullScreenState] Optional full screen
position in
- * which to open the map.
- * @param {number} [mapData.fullScreenState.zoom]
- * @param {number} [mapData.fullScreenState.latitude]
- * @param {number} [mapData.fullScreenState.longitude]
- */
- MapDialog.prototype.changeMap = function ( mapData ) {
- var fullScreenState, extendedData,
- existing = this.mapData;
-
- // Check whether it is the same map.
- if ( existing &&
- typeof existing.maptagId === 'number' &&
- existing.maptagId === mapData.maptagId ) {
-
- fullScreenState = mapData.fullScreenState;
- extendedData = {};
-
- // override with full screen state
- $.extend( extendedData, mapData, fullScreenState );
-
- // Use this boolean to stop listening to `moveend`
event while we're
- // manually moving the map.
- this.movingMap = true;
- this.MWMap.setView( [ extendedData.latitude,
extendedData.longitude ], extendedData.zoom );
- this.movingMap = false;
- return;
- }
-
- this.setup.call( this, mapData );
- this.ready.call( this, mapData );
- };
-
- MapDialog.prototype.getActionProcess = function ( action ) {
- var dialog = this;
- if ( !action ) {
- return new OO.ui.Process( function () {
- if ( router.getPath() !== '' ) {
- router.navigate( '' );
- } else {
- // force close
- dialog.close( { action: action } );
- }
- } );
- }
- return MapDialog.super.prototype.getActionProcess.call( this,
action );
- };
-
- /**
- * Tells the router to navigate to the current full screen map route.
- */
- MapDialog.prototype.updateHash = function () {
- var hash = kartographer.getMapHash( this.mapData, this.map );
-
- // Avoid extra operations
- if ( this.lastHash !== hash ) {
- router.navigate( hash );
- this.lastHash = hash;
- }
- };
-
- /**
- * Listens to `moveend` event and calls {@link #updateHash}.
- *
- * This method is throttled, meaning the method will be called at most
once per
- * every 100 milliseconds.
- */
- MapDialog.prototype.onMapMove = OO.ui.throttle( function () {
- // Stop listening to `moveend` event while we're
- // manually moving the map (updating from a hash),
- // or if the map is not yet loaded.
- /*jscs:disable disallowDanglingUnderscores */
- if ( this.movingMap || !this.map || !this.map._loaded ) {
- return false;
- }
- /*jscs:enable disallowDanglingUnderscores */
- this.updateHash();
- }, 100 );
-
- MapDialog.prototype.getSetupProcess = function ( mapData ) {
- return MapDialog.super.prototype.getSetupProcess.call( this,
mapData )
- .next( function () {
-
- if ( this.map ) {
- this.map.remove();
- this.$map.remove();
- }
-
- this.$map = $( '<div>' )
- .addClass(
'mw-kartographer-mapDialog-map' )
- .appendTo( this.$body );
-
- this.MWMap = kartoLive.MWMap( this.$map[ 0 ],
mapData );
- }, this );
- };
-
- MapDialog.prototype.getReadyProcess = function ( data ) {
- return MapDialog.super.prototype.getReadyProcess.call( this,
data )
- .next( function () {
- var self = this;
- this.MWMap.ready( function ( map, mapData ) {
- var fullScreenState =
mapData.fullScreenState,
- extendedData = {};
-
- self.map = map;
- self.mapData = mapData;
-
- map.addControl( new CloseControl( {
dialog: self } ) );
-
- if ( fullScreenState ) {
- // override with full screen
state
- $.extend( extendedData,
mapData, fullScreenState );
- map.setView( new L.LatLng(
extendedData.latitude, extendedData.longitude ), extendedData.zoom );
- }
-
- if ( typeof mapData.maptagId ===
'number' ) {
- map.on( 'moveend',
self.onMapMove, self );
- }
-
- mw.hook( 'wikipage.maps' ).fire( map,
true /* isFullScreen */ );
- } );
- }, this );
- };
-
- MapDialog.prototype.getTeardownProcess = function ( data ) {
- return MapDialog.super.prototype.getTeardownProcess.call( this,
data )
- .next( function () {
- this.map.remove();
- this.$map.remove();
- this.map = null;
- this.mapData = null;
- this.$map = null;
- }, this );
- };
-
- return function () {
- return new MapDialog();
- };
-
-} )(
- jQuery,
- mediaWiki,
- require( 'ext.kartographer.init' ),
- require( 'ext.kartographer.live' ),
- require( 'mediawiki.router' ),
- module.FullScreenCloseControl
-);
diff --git a/modules/fullscreen/index.js b/modules/fullscreen/index.js
deleted file mode 100644
index 9b3da29..0000000
--- a/modules/fullscreen/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* globals module */
-/**
- * Module containing elements required to open a map in full screen mode.
- *
- * @alias Fullscreen
- * @alias ext.kartographer.fullscreen
- * @class Kartographer.Fullscreen
- * @singleton
- */
-module.exports = {
- /**
- * @type {Kartographer.Fullscreen.CloseControl}
- */
- FullScreenCloseControl: module.FullScreenCloseControl,
-
- /**
- * @type {Kartographer.Fullscreen.MapDialog}
- */
- MapDialog: module.MapDialog
-};
diff --git a/modules/fullscreen/indexRoute.js b/modules/fullscreen/indexRoute.js
deleted file mode 100644
index bb11d25..0000000
--- a/modules/fullscreen/indexRoute.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* globals module, require */
-/**
- * Module executing code to add an index "" route that closes the map dialog.
- *
- * @alias indexRoute
- * @class Kartographer.Fullscreen.indexRoute
- * @private
- */
-module.indexRoute = ( function ( kartographer, router ) {
-
- // Add index route.
- router.route( '', function () {
- // TODO: mapDialog is undefined
- if ( kartographer.getMapDialog() ) {
- kartographer.getMapDialog().close();
- }
- } );
-
-} )( require( 'ext.kartographer.init' ), require( 'mediawiki.router' ) );
diff --git a/modules/kartographer.js b/modules/kartographer.js
deleted file mode 100644
index 437e461..0000000
--- a/modules/kartographer.js
+++ /dev/null
@@ -1,238 +0,0 @@
-/* globals module */
-/**
- * This module contains utility methods that may be useful to all other
- * modules.
- *
- * @alias ext.kartographer.init
- * @class Kartographer
- * @singleton
- */
-( function ( $, mw ) {
-
- var windowManager, mapDialog, openFullscreenMap;
-
- function getWindowManager() {
- if ( !windowManager ) {
- windowManager = new OO.ui.WindowManager();
- setMapDialog( mw.loader.require(
'ext.kartographer.fullscreen' ).MapDialog() );
- $( 'body' ).append( windowManager.$element );
- windowManager.addWindows( [ getMapDialog() ] );
- }
- return windowManager;
- }
-
- /**
- * Opens a full screen map.
- *
- * This method loads dependencies asynchronously. While these scripts
are
- * loading, more calls to this method can be made. We only need to
resolve
- * the last one. To make sure we only load the last map requested, we
keep
- * an increment of the calls being made.
- *
- * @param {L.Map|Object} mapData Map object to get data from, or raw
map data.
- * @param {Object} [fullScreenState] Optional full screen position in
which to
- * open the map.
- * @param {number} [fullScreenState.zoom]
- * @param {number} [fullScreenState.latitude]
- * @param {number} [fullScreenState.longitude]
- */
- openFullscreenMap = ( function () {
-
- var counter = -1;
-
- return function ( mapData, fullScreenState ) {
- var id = ++counter;
-
- mw.loader.using( 'ext.kartographer.fullscreen' ).done(
function () {
-
- var map, dialogData = {};
-
- if ( counter > id ) {
- return;
- }
-
- if ( mapData instanceof L.Map ) {
- map = mapData;
- mapData = getMapData( $(
map.getContainer() ).closest( '.mw-kartographer-interactive' ) );
- }
-
- $.extend( dialogData, mapData, {
- fullScreenState: fullScreenState,
- enableFullScreenButton: false
- } );
-
- if ( getMapDialog() ) {
- getMapDialog().changeMap( dialogData );
- return;
- }
- getWindowManager()
- .openWindow( getMapDialog(), dialogData
)
- .then( function ( opened ) {
- // It takes 250ms for the
dialog to open,
- // we'd better invalidate the
size once it opened.
- setTimeout( function () {
- var map =
getMapDialog().map;
- if ( map ) {
-
map.invalidateSize();
- }
- }, 300 );
- return opened;
- } )
- .then( function ( closing ) {
- var dialog = getMapDialog();
- if ( map ) {
- map.setView(
-
dialog.map.getCenter(),
-
dialog.map.getZoom()
- );
- }
- setMapDialog( null );
- windowManager = null;
- return closing;
- } );
- } );
- };
- } )();
-
- /**
- * 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.
- *
- * @param {Object} data Map data.
- * @param {L.Map} [map] When a map object is passed, the method will
- * read the current zoom and center from the map object.
- * @return {string} The route to open the map in full screen mode.
- */
- function getMapHash( data, map ) {
-
- var hash = '/' + ( data.isMapframe ? 'map' : 'maplink' ),
- mapPosition,
- newHash,
- initialHash = getScaleCoords( data.zoom, data.latitude,
data.longitude ).join( '/' );
-
- hash += '/' + data.maptagId;
-
- if ( map ) {
- mapPosition = getMapPosition( map );
- newHash = getScaleCoords( mapPosition.zoom,
mapPosition.latitude, mapPosition.longitude ).join( '/' );
-
- if ( newHash !== initialHash ) {
- hash += '/' + newHash;
- }
- }
-
- return hash;
- }
-
- /**
- * Convenient method that gets the current position of the map.
- *
- * @return {Object} Object with the zoom, the latitude and the
longitude.
- * @return {number} return.zoom
- * @return {number} return.latitude
- * @return {number} return.longitude
- */
- function getMapPosition( map ) {
- var center = map.getCenter();
- return { zoom: map.getZoom(), latitude: center.lat, longitude:
center.lng };
- }
-
- /**
- * Convenient method that formats the coordinates based on the zoom
level.
- *
- * @param {number} zoom
- * @param {number} lat
- * @param {number} lng
- * @return {Array} Array with the zoom (number), the latitude (string)
and
- * the longitude (string).
- */
- function getScaleCoords( zoom, lat, lng ) {
- var precisionPerZoom = [ 0, 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4,
4, 4, 4, 4, 5, 5 ];
-
- return [
- zoom,
- lat.toFixed( precisionPerZoom[ zoom ] ),
- lng.toFixed( precisionPerZoom[ zoom ] )
- ];
- }
-
- /**
- * Gets the map data attached to an element.
- *
- * @param {HTMLElement} element Element
- * @return {Object|null} Map properties
- * @return {number} return.latitude
- * @return {number} return.longitude
- * @return {number} return.zoom
- * @return {string} return.style Map style
- * @return {string[]} return.overlays Overlay groups
- */
- function getMapData( element ) {
- var $el = $( element ),
- maptagId = null;
- // Prevent users from adding map divs directly via wikitext
- if ( $el.attr( 'mw-data' ) !== 'interface' ) {
- return null;
- }
-
- if ( $.type( $el.data( 'maptag-id' ) ) !== 'undefined' ) {
- maptagId = +$el.data( 'maptag-id' );
- }
-
- return {
- isMapframe: $el.hasClass( 'mw-kartographer-interactive'
),
- maptagId: maptagId,
- latitude: +$el.data( 'lat' ),
- longitude: +$el.data( 'lon' ),
- zoom: +$el.data( 'zoom' ),
- style: $el.data( 'style' ),
- overlays: $el.data( 'overlays' ) || []
- };
- }
-
- /**
- * Formats the fullscreen state object based on route attributes.
- *
- * @param {string|number} [zoom]
- * @param {string|number} [latitude]
- * @param {string|number} [longitude]
- * @return {Object} Full screen state
- * @return {number} [return.zoom] Zoom if between 0 and 18.
- * @return {number} [return.latitude]
- * @return {number} [return.longitude]
- */
- function getFullScreenState( zoom, latitude, longitude ) {
- var obj = {};
- if ( zoom !== undefined && zoom >= 0 && zoom <= 18 ) {
- obj.zoom = +zoom;
- }
- if ( longitude !== undefined ) {
- obj.latitude = +latitude;
- obj.longitude = +longitude;
- }
- return obj;
- }
-
- function getMapDialog() {
- return mapDialog;
- }
-
- function setMapDialog( dialog ) {
- mapDialog = dialog;
- return mapDialog;
- }
-
- module.exports = {
- getMapHash: getMapHash,
- openFullscreenMap: openFullscreenMap,
- getMapData: getMapData,
- getMapPosition: getMapPosition,
- getFullScreenState: getFullScreenState,
- getMapDialog: getMapDialog,
- getScaleCoords: getScaleCoords
- };
-
-}( jQuery, mediaWiki ) );
diff --git a/modules/live/FullScreenControl.js
b/modules/live/FullScreenControl.js
deleted file mode 100644
index ba59d1c..0000000
--- a/modules/live/FullScreenControl.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/* globals module, require */
-/**
- * Control to display the map in full screen mode.
- *
- * See [L.Control](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-control/)
- * documentation for more details.
- *
- * @alias FullScreenControl
- * @class Kartographer.Live.FullScreenControl
- * @extends L.Control
- */
-module.FullScreenControl = ( function ( kartographer, router ) {
-
- return L.Control.extend( {
- options: {
- // Do not switch for RTL because zoom also stays in
place
- position: 'topright'
- },
-
- onAdd: function ( map ) {
- var container = L.DomUtil.create( 'div', 'leaflet-bar
leaflet-control-static' );
-
- this.link = L.DomUtil.create( 'a',
'oo-ui-icon-fullScreen', container );
- this.link.title = mw.msg(
'kartographer-fullscreen-text' );
- this.map = map;
-
- this.map.on( 'moveend', this.onMapMove, this );
- if ( !router.isSupported() ) {
- L.DomEvent.addListener( this.link, 'click',
this.onShowFullScreen, this );
- }
- L.DomEvent.disableClickPropagation( container );
- this.updateHash();
-
- return container;
- },
-
- onMapMove: function () {
- /*jscs:disable disallowDanglingUnderscores */
- if ( !this.map._loaded ) {
- return false;
- }
- /*jscs:enable disallowDanglingUnderscores */
- this.updateHash();
- },
-
- updateHash: function () {
- var hash = kartographer.getMapHash(
this.options.mapData, this.map );
- this.link.href = '#' + hash;
- },
-
- onShowFullScreen: function ( e ) {
- L.DomEvent.stop( e );
- kartographer.openFullscreenMap( this.map,
kartographer.getMapPosition( this.map ) );
- }
- } );
-} )( require( 'ext.kartographer.init' ), require( 'mediawiki.router' ) );
diff --git a/modules/live/MWMap.js b/modules/live/MWMap.js
deleted file mode 100644
index f53aef5..0000000
--- a/modules/live/MWMap.js
+++ /dev/null
@@ -1,552 +0,0 @@
-/* globals module */
-/**
- * Mediawiki Map class.
- *
- * @alias MWMap
- * @class Kartographer.Live.MWMap
- */
-module.MWMap = ( function ( FullScreenControl, dataLayerOpts, ControlScale ) {
-
- var scale, urlFormat,
- mapServer = mw.config.get( 'wgKartographerMapServer' ),
- worldLatLng = new L.LatLngBounds( [ -90, -180 ], [ 90, 180 ] ),
- MWMap;
-
- 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: 1000,
- sleepNote: false,
- sleepOpacity: 1,
- // the default zoom applied when `longitude` and `latitude` were
- // specified, but zoom was not.
- fallbackZoom: 13
- } );
-
- /*jscs:disable disallowDanglingUnderscores */
- /**
- * @constructor
- * @param {HTMLElement} container Map container
- * @param {Object} data Map data
- * @param {number} data.latitude
- * @param {number} data.longitude
- * @param {number} data.zoom
- * @param {string} [data.style] Map style
- * @param {string[]} [data.overlays] Names of overlay groups to show
- * @param {boolean} [data.enableFullScreenButton] add zoom
- * @type Kartographer.Live.MWMap
- */
- MWMap = function ( container, data ) {
- /**
- * Reference to the map container.
- *
- * @type {HTMLElement}
- */
- this.container = container;
-
- /**
- * Reference to the map container as a jQuery element.
- *
- * @type {jQuery}
- */
- this.$container = $( container );
- this.$container.addClass( 'mw-kartographer-map' );
-
- /**
- * The map data that configured this map.
- *
- * Please do not access this property directly.
- * Prefer {@link #ready} to access the object.
- *
- * @type {jQuery}
- * @protected
- */
- this._data = data;
-
- /**
- * Reference to the Leaflet/Mapbox map object.
- *
- * Please do not access this property directly.
- * Prefer {@link #ready} to access the object.
- *
- * @type {L.Map}
- * @protected
- */
- this.map = L.map( this.container );
-
- if ( !this.container.clientWidth ) {
- this._fixMapSize();
- }
-
- this.loaded = this._initMap();
- };
-
- /**
- * Initializes the map:
- *
- * - Adds the base tile layer
- * - Fetches groups data
- * - Adds data layers
- *
- * @return {jQuery.Promise} Promise which resolves once the map is
- * initialized.
- * @private
- */
- MWMap.prototype._initMap = function () {
- var style = this._data.style || mw.config.get(
'wgKartographerDfltStyle' );
-
- /**
- * @property {L.TileLayer} Reference to `Wikimedia` tile layer.
- * @ignore
- */
- this.map.wikimediaLayer = L.tileLayer( mapServer + '/' + style
+ urlFormat, {
- maxZoom: 18,
- attribution: mw.message( 'kartographer-attribution'
).parse()
- } ).addTo( this.map );
-
- return this._initGroupData();
- };
-
- /**
- * Primary function to use right after you instantiated a `MWMap`
- * to get the map ready and be able to extend the {@link #map} object.
- *
- * Use it to add handlers to be called, once the map is ready, with
- * the {@link #map map object} and updated {@link #_data map data}.
- *
- * *Notes on updated map data:*
- * - During the "init" phase, the map downloads its data asynchronously.
- * - During the "ready" phase, the map positions its center and sets its
- * zoom according to the initial map data. The map is able to position
- * itself and calculate its best zoom in case longitude/latitude/zoom
were
- * not explicitly defined. See with the code below how you can retrieve
- * the `map` and `mapData` objects:
- *
- * var MWMap = new MWMap( this.$map[ 0 ], initialMapData );
- * MWMap.ready(function(map, mapData) {
- * // `map` is the map object
- * // `mapData` is the updated mapData object.
- * });
- *
- * @param {jQuery.Promise} promise Promise which resolves with the map
object and
- * the updated map data once the map is ready.
- * @return {jQuery.Promise.then} Function to add handlers to be
- * called once the map is ready.
- */
- MWMap.prototype.ready = function ( promise ) {
- this.ready = $.when( this.loaded )
- .then( this._initMapPosition.bind( this ) )
- .then( this._initControls )
- .then( this._fixCollapsibleMaps )
- .then( this._invalidateInterative )
- .then( promise )
- .then;
- return this.ready;
- };
-
- /**
- * Initializes map data:
- *
- * - Fetches groups data
- * - Adds data layers
- *
- * @return {jQuery.Promise} Promise which resolves once the data is
loaded
- * and the data layers added to the map.
- * @private
- */
- MWMap.prototype._initGroupData = function () {
- var deferred = $.Deferred(),
- self = this,
- map = this.map,
- data = this._data;
-
- /**
- * @property {Object} dataLayers Hash map of data groups and
their corresponding
- * {@link L.mapbox.FeatureLayer layers}.
- * @ignore
- */
- map.dataLayers = {};
-
- data.overlays = data.overlays || [];
-
- getMapGroupData( data.overlays ).then( function ( mapData ) {
- $.each( data.overlays, function ( index, group ) {
- if ( !$.isEmptyObject( mapData[ group ] ) ) {
- map.dataLayers[ group ] =
self.addDataLayer( mapData[ group ] );
- } else {
- mw.log.warn( 'Layer not found or
contains no data: "' + group + '"' );
- }
- } );
- deferred.resolve().promise();
- } );
- return deferred.promise();
- };
-
- /**
- * Init map position with {@link #setView}.
- *
- * @return {jQuery.Promise} Promise which resolves with the updated map
- * data.
- * @private
- */
- MWMap.prototype._initMapPosition = function () {
- var data = this._data;
-
- this.setView( [ data.latitude, data.longitude ], data.zoom,
true, true );
- return $.Deferred().resolveWith( this, [ this.map, this._data ]
).promise();
- };
-
- /**
- * Init map controls.
- *
- * - Adds the "attribution" control.
- * - Adds the "full screen" button control when
`enableFullScreenButton` is
- * truthy.
- *
- * @return {jQuery.Promise}
- * @private
- */
- MWMap.prototype._initControls = function () {
- this.map.attributionControl.setPrefix( '' );
-
- if ( this._data.enableFullScreenButton ) {
- this.map.addControl( new FullScreenControl( {
- mapData: this._data
- } ) );
- }
-
- this.map.addControl( new ControlScale( { position:
'bottomright' } ) );
-
- return $.Deferred().resolveWith( this, [ this.map, this._data ]
).promise();
- };
-
- /**
- * Special case for collapsible maps.
- * When the container is hidden Leaflet is not able to
- * calculate the expected size when visible. We need to force
- * updating the map to the new container size on `expand`.
-
- * @return {jQuery.Promise}
- * @private
- */
- MWMap.prototype._fixCollapsibleMaps = function () {
- var map = this.map;
-
- if ( !this.$container.is( ':visible' ) ) {
- this.$container.closest( '.mw-collapsible' )
- .on( 'afterExpand.mw-collapsible', function () {
- map.invalidateSize();
- } );
- }
- return $.Deferred().resolveWith( this, [ this.map, this._data ]
).promise();
- };
-
- /**
- * 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.
- *
- * @return {jQuery.Promise}
- * @private
- */
- MWMap.prototype._invalidateInterative = function () {
- var deferred = $.Deferred().resolveWith( this, [ this.map,
this._data ] ),
- map = this.map,
- toggleStaticMap;
-
- if ( !this._data.enableFullScreenButton ) {
- // skip if the map is full screen
- return deferred.promise();
- }
- // add Leaflet.Sleep when the map isn't full screen.
- map.addHandler( 'sleep', L.Map.Sleep );
-
- // `toggleStaticMap` should be debounced for performance.
- toggleStaticMap = L.bind( toggleStaticState, null, map );
-
- // `invalidateSize` is triggered on window `resize` events.
- map.invalidateSize = function ( options ) {
- L.Map.prototype.invalidateSize.call( map, options );
- // Local debounce because oojs is not yet available.
- if ( map._staticTimer ) {
- clearTimeout( map._staticTimer );
- }
- map._staticTimer = setTimeout( toggleStaticMap, 200 );
- };
- // Initialize static state.
- toggleStaticMap();
-
- return deferred.promise();
- };
-
- /**
- * Creates a new GeoJSON layer and adds it to the map.
- *
- * @param {Object} geoJson
- */
- MWMap.prototype.addDataLayer = function ( geoJson ) {
- try {
- return L.mapbox.featureLayer( geoJson, dataLayerOpts
).addTo( this.map );
- } catch ( e ) {
- mw.log( e );
- }
- };
-
- /**
- * Fixes map size when the container is not visible yet, thus has no
- * physical size.
- *
- * The hack is to try jQuery which will pick up CSS dimensions. T125263
- *
- * However in full screen, the container size is actually [0,0] at that
- * time. In that case, the script looks for the first visible parent and
- * takes its `height` and `width` to initialize the map.
- *
- * @private
- */
- MWMap.prototype._fixMapSize = function () {
- var width, height, $visibleParent = this.$container.closest(
':visible' );
-
- // Get `max` properties in case the container was wrapped
- // with {@link #responsiveContainerWrap}.
- 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.map._size = new L.Point( width, height );
- };
-
- /**
- * 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|Array|null} [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.
- */
- MWMap.prototype.setView = function ( center, zoom, options, save ) {
- var maxBounds,
- map = this.map,
- data = this._data;
-
- try {
- center = L.latLng( center );
- zoom = isNaN( zoom ) ? this.map.options.fallbackZoom :
zoom;
- map.setView( center, zoom, options );
- } catch ( e ) {
- // Determines best center of the map
- maxBounds = getValidBounds( map );
-
- if ( maxBounds.isValid() ) {
- map.fitBounds( maxBounds );
- } else {
- map.fitWorld();
- }
- // (Re-)Applies expected zoom
- if ( !isNaN( data.zoom ) ) {
- map.setZoom( data.zoom );
- }
- if ( save ) {
- // Updates map data.
- data.zoom = map.getZoom();
- data.longitude = map.getCenter().lng;
- data.latitude = map.getCenter().lat;
- // Updates container's data attributes to avoid
`NaN` errors
- $( map.getContainer() ).closest(
'.mw-kartographer-interactive' ).data( {
- zoom: data.zoom,
- lon: data.longitude,
- lat: data.latitude
- } );
- }
- }
- };
-
- /**
- * Returns the map data for the page.
- *
- * If the data is not already loaded (`wgKartographerLiveData`), an
- * asynchronous request will be made to fetch the missing groups.
- * The new data is then added to `wgKartographerLiveData`.
- *
- * @param {string[]} overlays Overlay group names
- * @return {jQuery.Promise} Promise which resolves with the group data,
an object keyed by group name
- * @private
- */
- function getMapGroupData( overlays ) {
- var deferred = $.Deferred(),
- groupsLoaded = mw.config.get( 'wgKartographerLiveData'
),
- groupsToLoad = [],
- promises = [];
-
- if ( !groupsLoaded ) {
- // Keep the reference to groupsLoaded, as it shouldn't
change again
- groupsLoaded = {};
- mw.config.set( 'wgKartographerLiveData', groupsLoaded );
- }
-
- // For each requested layer, make sure it is loaded or is
promised to be loaded
- $( overlays ).each( function ( key, value ) {
- var data = groupsLoaded[ value ];
- if ( data === undefined ) {
- groupsToLoad.push( value );
- // Once loaded, this value will be replaced
with the received data
- groupsLoaded[ value ] = deferred.promise();
- } else if ( data !== null && $.isFunction( data.then )
) {
- promises.push( data );
- }
- } );
-
- if ( groupsToLoad.length ) {
- promises.push( deferred.promise() );
- }
- if ( !promises.length ) {
- return deferred.resolve( groupsLoaded ).promise();
- }
-
- new mw.Api().get( {
- action: 'query',
- formatversion: '2',
- titles: mw.config.get( 'wgPageName' ),
- prop: 'mapdata',
- mpdgroups: groupsToLoad.join( '|' )
- } ).done( function ( data ) {
- var rawMapData = data.query.pages[ 0 ].mapdata,
- mapData = rawMapData && JSON.parse( rawMapData
) || {};
- $.extend( groupsLoaded, mapData );
- deferred.resolve( groupsLoaded );
- } );
-
- return $.when.apply( $, promises ).then( function () {
- // All pending promises are done
- return groupsLoaded;
- } ).promise();
- }
-
- /**
- * 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;
- }
-
- /**
- * 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`
- *
- * @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;
- }
- return false;
- }
-
- /**
- * Makes the map interactive IIF :
- *
- * - the `device width > 480px`,
- * - there is at least a 200px horizontal margin.
- *
- * Otherwise makes it static.
- *
- * @param {L.Map} map
- * @private
- */
- function toggleStaticState( map ) {
- 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 || ( map.getSize().x + 200 ) >
deviceWidth;
-
- // Skip if the map is already static
- if ( map._static === staticMap ) {
- return;
- }
-
- // Toggle static/interactive state of the map
- map._static = staticMap;
-
- if ( staticMap ) {
- map.sleep._sleepMap();
- map.sleep.disable();
- map.scrollWheelZoom.disable();
- } else {
- map.sleep.enable();
- }
- $( map.getContainer() ).toggleClass( 'mw-kartographer-static',
staticMap );
- }
-
- return function ( container, data ) {
- return new MWMap( container, data );
- };
-} )(
- module.FullScreenControl,
- module.dataLayerOpts,
- module.ControlScale
-);
diff --git a/modules/live/dataLayerOpts.js b/modules/live/dataLayerOpts.js
deleted file mode 100644
index d00b91f..0000000
--- a/modules/live/dataLayerOpts.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* globals module */
-/**
- * Contains a set of options passed to mapbox when adding a feature layer (see
available options).
- *
- * See
[L.mapbox.featureLayer](https://www.mapbox.com/mapbox.js/api/v2.3.0/l-mapbox-featurelayer/)
- * documentation for the full list of options.
- *
- * @alias dataLayerOpts
- * @class Kartographer.Live.dataLayerOpts
- * @singleton
- * @private
- */
-module.dataLayerOpts = {
- // Disable double-sanitization by mapbox's internal sanitizer
- // because geojson has already passed through the MW internal sanitizer
- sanitizer: function ( v ) {
- return v;
- }
-};
diff --git a/modules/live/index.js b/modules/live/index.js
deleted file mode 100644
index 659c099..0000000
--- a/modules/live/index.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* globals module */
-/**
- * Module containing elements required to display an interactive map on the
- * page.
- *
- * ```
- * mw.loader.using( 'ext.kartographer.live', function() {
- *
- * var kartographer = mw.loader.require('ext.kartographer.init'),
- * kartoLive = mw.loader.require('ext.kartographer.live'),
- *
- * // Get map container
- * mapContainer = document.getElementById('#map-selector'),
- *
- * // Get initial configuration
- * initialMapData = kartographer.getMapData( mapContainer ),
- * kartoLiveMap;
- *
- * // Start the "init" phase.
- * kartoLiveMap = kartoLive.MWMap( mapContainer, initialMapData );
- *
- * // Bind the "ready" hook
- * kartoLiveMap.ready( function( map, mapData ) {
- * console.log( 'The map was created successfully !' );
- * console.log( '- Kartographer.Live "map" object: ', kartoLiveMap
);
- * console.log( '- Leaflet/Mapbox "map" object: ', map );
- * console.log( '- "mapData" object: ', mapData );
- * } );
- * } );
- * ```
- *
- * @alias Live
- * @alias ext.kartographer.live
- * @class Kartographer.Live
- * @singleton
- */
-module.exports = {
- /**
- * @type {Kartographer.Live.FullScreenControl}
- */
- FullScreenControl: module.FullScreenControl,
-
- /**
- * @type {Kartographer.Live.ControlScale}
- */
- ControlScale: module.ControlScale,
-
- /**
- * @type {Kartographer.Live.MWMap}
- */
- MWMap: module.MWMap
-};
diff --git a/modules/mapframe/mapframe.js b/modules/mapframe/mapframe.js
index 5921348..90332fa 100644
--- a/modules/mapframe/mapframe.js
+++ b/modules/mapframe/mapframe.js
@@ -10,7 +10,7 @@
* @class Kartographer.Frame
* @singleton
*/
-module.exports = ( function ( $, mw, kartographer, kartoLive, router ) {
+module.exports = ( function ( $, mw, kartobox, router ) {
/**
* References the mapframe containers of the page.
@@ -20,74 +20,62 @@
var maps = [];
/**
- * Wraps a map container to make it (and its map) responsive on
- * mobile (MobileFrontend).
+ * Gets the map data attached to an element.
*
- * The initial `mapContainer`:
+ * @param {HTMLElement} element Element
+ * @return {Object|null} Map properties
+ * @return {number} return.latitude
+ * @return {number} return.longitude
+ * @return {number} return.zoom
+ * @return {string} return.style Map style
+ * @return {string[]} return.overlays Overlay groups
+ */
+ function getMapData( element ) {
+ var $el = $( element );
+ // Prevent users from adding map divs directly via wikitext
+ if ( $el.attr( 'mw-data' ) !== 'interface' ) {
+ return null;
+ }
+
+ return {
+ latitude: +$el.data( 'lat' ),
+ longitude: +$el.data( 'lon' ),
+ zoom: +$el.data( 'zoom' ),
+ style: $el.data( 'style' ),
+ overlays: $el.data( 'overlays' ) || []
+ };
+ }
+
+ /**
+ * Formats center if valid.
*
- * <div class="mw-kartographer-interactive" style="height: Y;
width: X;">
- * <!-- this is the component carrying Leaflet.Map -->
- * </div>
- *
- * Becomes :
- *
- * <div class="mw-kartographer-interactive
mw-kartographer-responsive" style="max-height: Y; max-width: X;">
- * <div class="mw-kartographer-responder"
style="padding-bottom: (100*Y/X)%">
- * <div>
- * <!-- this is the component carrying Leaflet.Map -->
- * </div>
- * </div>
- * </div>
- *
- * **Note:** the container that carries the map data remains the initial
- * `mapContainer` passed in arguments. Its selector remains
`.mw-kartographer-interactive`.
- * However it is now a sub-child that carries the map.
- *
- * **Note 2:** the CSS applied to these elements vary whether the map
width
- * is absolute (px) or relative (%). The example above describes the
absolute
- * width case.
- *
- * @param {HTMLElement} mapContainer Initial component to carry the map.
- * @return {HTMLElement} New map container to carry the map.
+ * @param {string|number} latitude
+ * @param {string|number} longitude
+ * @return {Array|undefined}
* @private
*/
- function responsiveContainerWrap( mapContainer ) {
- var $container = $( mapContainer ),
- $responder, $map,
- width = mapContainer.style.width,
- isRelativeWidth = width.slice( -1 ) === '%',
- height = +( mapContainer.style.height.slice( 0, -2 ) ),
- containerCss, responderCss;
+ function validCenter( latitude, longitude ) {
+ latitude = +latitude;
+ longitude = +longitude;
- // Convert the value to a string.
- width = isRelativeWidth ? width : +( width.slice( 0, -2 ) );
-
- if ( isRelativeWidth ) {
- containerCss = {};
- responderCss = {
- // The inner container must occupy the full
height
- height: height
- };
- } else {
- containerCss = {
- // Remove explicitly set dimensions
- width: '',
- height: '',
- // Prevent over-sizing
- 'max-width': width,
- 'max-height': height
- };
- responderCss = {
- // Use padding-bottom trick to maintain
original aspect ratio
- 'padding-bottom': ( 100 * height / width ) + '%'
- };
+ if ( !isNaN( latitude ) && !isNaN( longitude ) ) {
+ return [ latitude, longitude ];
}
- $container.addClass( 'mw-kartographer-responsive' ).css(
containerCss );
- $responder = $( '<div>' ).addClass( 'mw-kartographer-responder'
).css( responderCss );
+ }
- $map = $( '<div>' );
- $container.append( $responder.append( $map ) );
- return $map[ 0 ];
+ /**
+ * Formats zoom if valid.
+ *
+ * @param {string|number} zoom
+ * @return {number|undefined}
+ * @private
+ */
+ function validZoom( zoom ) {
+ zoom = +zoom;
+
+ if ( !isNaN( zoom ) ) {
+ return zoom;
+ }
}
/**
@@ -97,41 +85,31 @@
*/
mw.hook( 'wikipage.content' ).add( function ( $content ) {
var mapsInArticle = [],
- isMobile = mw.config.get( 'skin' ) === 'minerva',
promises = [];
$content.find( '.mw-kartographer-interactive' ).each( function
( index ) {
- var MWMap, data,
+ var map, data,
container = this,
- $container = $( this );
+ deferred = $.Deferred();
- $container.data( 'maptag-id', index );
- data = kartographer.getMapData( container );
+ data = getMapData( container );
if ( data ) {
data.enableFullScreenButton = true;
- if ( isMobile ) {
- container = responsiveContainerWrap(
container );
- }
-
- MWMap = kartoLive.MWMap( container, data );
- MWMap.ready( function ( map, mapData ) {
-
- map.doubleClickZoom.disable();
-
- mapsInArticle.push( map );
- maps[ index ] = map;
-
- map.on( 'dblclick', function () {
- if ( router.isSupported() ) {
- router.navigate(
kartographer.getMapHash( mapData, map ) );
- } else {
-
kartographer.openFullscreenMap( map, kartographer.getMapPosition( map ) );
- }
- } );
+ map = kartobox.map( {
+ container: container,
+ center: validCenter( data.latitude,
data.longitude ),
+ zoom: validZoom( data.zoom ),
+ fullScreenRoute: '/map/' + index,
+ allowFullScreen: true,
+ dataGroups: data.overlays
} );
- promises.push( MWMap.ready );
+
+ mapsInArticle.push( map );
+ maps[ index ] = map;
+
+ promises.push( deferred.promise() );
}
} );
@@ -146,12 +124,16 @@
// #/map/0/16/-122.4006/37.7873
router.route(
/map\/([0-9]+)(?:\/([0-9]+))?(?:\/([\-\+]?\d+\.?\d{0,5})?\/([\-\+]?\d+\.?\d{0,5})?)?/,
function ( maptagId, zoom, latitude, longitude ) {
var map = maps[ maptagId ];
+
if ( !map ) {
router.navigate( '' );
return;
}
- kartographer.openFullscreenMap( map,
kartographer.getFullScreenState( zoom, latitude, longitude ) );
+ map.openFullScreen( {
+ center: validCenter( latitude,
longitude ),
+ zoom: validZoom( zoom )
+ } );
} );
// Check if we need to open a map in full screen.
@@ -163,7 +145,6 @@
} )(
jQuery,
mediaWiki,
- require( 'ext.kartographer.init' ),
- require( 'ext.kartographer.live' ),
+ require( 'ext.kartographer.box' ),
require( 'mediawiki.router' )
);
diff --git a/modules/maplink/maplink.js b/modules/maplink/maplink.js
index e107106..e37c41d 100644
--- a/modules/maplink/maplink.js
+++ b/modules/maplink/maplink.js
@@ -10,7 +10,7 @@
* @class Kartographer.Link
* @singleton
*/
-module.exports = ( function ( $, mw, kartographer, router ) {
+module.exports = ( function ( $, mw, router, kartobox ) {
/**
* References the maplinks of the page.
@@ -18,6 +18,65 @@
* @type {HTMLElement[]}
*/
var maplinks = [];
+
+ /**
+ * Gets the map data attached to an element.
+ *
+ * @param {HTMLElement} element Element
+ * @return {Object|null} Map properties
+ * @return {number} return.latitude
+ * @return {number} return.longitude
+ * @return {number} return.zoom
+ * @return {string} return.style Map style
+ * @return {string[]} return.overlays Overlay groups
+ */
+ function getMapData( element ) {
+ var $el = $( element );
+ // Prevent users from adding map divs directly via wikitext
+ if ( $el.attr( 'mw-data' ) !== 'interface' ) {
+ return null;
+ }
+
+ return {
+ latitude: +$el.data( 'lat' ),
+ longitude: +$el.data( 'lon' ),
+ zoom: +$el.data( 'zoom' ),
+ style: $el.data( 'style' ),
+ overlays: $el.data( 'overlays' ) || []
+ };
+ }
+
+ /**
+ * Formats center if valid.
+ *
+ * @param {string|number} latitude
+ * @param {string|number} longitude
+ * @return {Array|undefined}
+ * @private
+ */
+ function validCenter( latitude, longitude ) {
+ latitude = +latitude;
+ longitude = +longitude;
+
+ if ( !isNaN( latitude ) && !isNaN( longitude ) ) {
+ return [ latitude, longitude ];
+ }
+ }
+
+ /**
+ * Formats zoom if valid.
+ *
+ * @param {string|number} zoom
+ * @return {number|undefined}
+ * @private
+ */
+ function validZoom( zoom ) {
+ zoom = +zoom;
+
+ if ( !isNaN( zoom ) ) {
+ return zoom;
+ }
+ }
/**
* This code will be executed once the article is rendered and ready.
@@ -29,11 +88,16 @@
// Some links might be displayed outside of $content, so we
need to
// search outside. This is an anti-pattern and should be
improved...
// Meanwhile #content is better than searching the full
document.
- $( '.mw-kartographer-link', '#content' ).each( function ( index
) {
- maplinks[ index ] = this;
+ $( '.mw-kartographer-maplink', '#content' ).each( function (
index ) {
+ var data = getMapData( this );
- $( this ).data( 'maptag-id', index );
- this.href = '#' + '/maplink/' + index;
+ maplinks[ index ] = kartobox.link( {
+ container: this,
+ center: data.latitude && data.latitude ? [
data.latitude, data.longitude ] : 'auto',
+ zoom: data.zoom || 'auto',
+ dataGroups: data.overlays,
+ fullScreenRoute: '/maplink/' + index
+ } );
} );
// Opens a maplink in full screen.
#/maplink(/:zoom)(/:latitude)(/:longitude)
@@ -42,15 +106,17 @@
// #/maplink/0/5
// #/maplink/0/16/-122.4006/37.7873
router.route(
/maplink\/([0-9]+)(?:\/([0-9]+))?(?:\/([\-\+]?\d+\.?\d{0,5})?\/([\-\+]?\d+\.?\d{0,5})?)?/,
function ( maptagId, zoom, latitude, longitude ) {
- var link = maplinks[ maptagId ],
- data;
+ var link = maplinks[ maptagId ];
if ( !link ) {
router.navigate( '' );
return;
}
- data = kartographer.getMapData( link );
- kartographer.openFullscreenMap( data,
kartographer.getFullScreenState( zoom, latitude, longitude ) );
+
+ link.openFullScreen( {
+ center: validCenter( latitude, longitude ),
+ zoom: validZoom( zoom )
+ } );
} );
// Check if we need to open a map in full screen.
@@ -61,6 +127,6 @@
} )(
jQuery,
mediaWiki,
- require( 'ext.kartographer.init' ),
- require( 'mediawiki.router' )
+ require( 'mediawiki.router' ),
+ require( 'ext.kartographer.box' )
);
diff --git a/modules/preview/preview.js b/modules/preview/preview.js
index 8c45a54..a9c3848 100644
--- a/modules/preview/preview.js
+++ b/modules/preview/preview.js
@@ -4,7 +4,7 @@
* the map to show the corresponding coordinates.
*
* This module may be loaded and executed by
- * {@link Kartographer.Live.enablePreview ext.kartographer.live}.
+ * {@link Kartographer.Box.enablePreview ext.kartographer.box}.
*
* @alias Preview
* @alias ext.kartographer.preview
diff --git a/modules/ve-maps/ve.ce.MWMapsNode.js
b/modules/ve-maps/ve.ce.MWMapsNode.js
index 6513522..b0632dc 100644
--- a/modules/ve-maps/ve.ce.MWMapsNode.js
+++ b/modules/ve-maps/ve.ce.MWMapsNode.js
@@ -4,7 +4,7 @@
* @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
*/
/* globals require */
-var kartoLive = require( 'ext.kartographer.live' ),
+var kartobox = require( 'ext.kartographer.box' ),
kartoEditing = require( 'ext.kartographer.editing' );
/**
@@ -114,10 +114,10 @@
if ( requiresInteractive ) {
if ( !this.map && this.getRoot() ) {
- mw.loader.using( 'ext.kartographer.live' ).then(
this.setupMap.bind( this ) );
+ mw.loader.using( 'ext.kartographer.box' ).then(
this.setupMap.bind( this ) );
} else if ( this.map ) {
- this.updateMapPosition();
this.updateGeoJson();
+ this.updateMapPosition();
}
} else {
if ( this.map ) {
@@ -133,6 +133,37 @@
.addClass( alignClasses[ align ] )
.css( this.model.getCurrentDimensions() );
};
+/**
+ * Formats center if valid.
+ *
+ * @param {string|number} latitude
+ * @param {string|number} longitude
+ * @return {Array|undefined}
+ * @private
+ */
+function validCenter( latitude, longitude ) {
+ latitude = +latitude;
+ longitude = +longitude;
+
+ if ( !isNaN( latitude ) && !isNaN( longitude ) ) {
+ return [ latitude, longitude ];
+ }
+}
+
+/**
+ * Formats zoom if valid.
+ *
+ * @param {string|number} zoom
+ * @return {number|undefined}
+ * @private
+ */
+function validZoom( zoom ) {
+ zoom = +zoom;
+
+ if ( !isNaN( zoom ) ) {
+ return zoom;
+ }
+}
/**
* Setup an interactive map
@@ -140,28 +171,26 @@
ve.ce.MWMapsNode.prototype.setupMap = function () {
var mwData = this.model.getAttribute( 'mw' ),
mwAttrs = mwData && mwData.attrs,
- latitude = +mwAttrs.latitude,
- longitude = +mwAttrs.longitude,
- zoom = +mwAttrs.zoom,
+ zoom = validZoom( +mwAttrs.zoom ),
+ center = validCenter( mwAttrs.latitude, mwAttrs.longitude ),
node = this;
- this.MWMap = kartoLive.MWMap( this.$element[ 0 ], {
- latitude: latitude,
- longitude: longitude,
+ node.map = kartobox.map( {
+ container: node.$element[ 0 ],
+ center: center,
zoom: zoom
// TODO: Support style editing
} );
- this.MWMap.ready( function ( map ) {
- node.map = map;
-
+ node.map.on( 'layeradd', node.updateMapPosition, node );
+ node.map.doWhenReady( function ( ) {
node.updateGeoJson();
// Disable interaction
- map.dragging.disable();
- map.touchZoom.disable();
- map.doubleClickZoom.disable();
- map.scrollWheelZoom.disable();
- map.keyboard.disable();
+ node.map.dragging.disable();
+ node.map.touchZoom.disable();
+ node.map.doubleClickZoom.disable();
+ node.map.scrollWheelZoom.disable();
+ node.map.keyboard.disable();
} );
};
@@ -184,9 +213,18 @@
ve.ce.MWMapsNode.prototype.updateMapPosition = function () {
var mwData = this.model.getAttribute( 'mw' ),
mapData = this.mapData,
- updatedData = mwData && mwData.attrs;
+ updatedData = mwData && mwData.attrs,
+ current;
- if (
+ if ( !validCenter( mapData.latitude, mapData.longitude ) ||
!updatedData ) {
+ // auto calculate the position
+ this.map.setView( null, mapData.zoom );
+ current = this.map.getMapPosition();
+ // update missing attributes with current position.
+ mwData.attrs.latitude = mapData.latitude =
current.center.lat.toString();
+ mwData.attrs.longitude = mapData.longitude =
current.center.lng.toString();
+ mwData.attrs.zoom = mapData.zoom = current.zoom.toString();
+ } else if (
mapData.latitude !== updatedData.latitude ||
mapData.longitude !== updatedData.longitude ||
mapData.zoom !== updatedData.zoom
diff --git a/modules/ve-maps/ve.ui.MWMapsDialog.js
b/modules/ve-maps/ve.ui.MWMapsDialog.js
index 3e183dc..e555c64 100644
--- a/modules/ve-maps/ve.ui.MWMapsDialog.js
+++ b/modules/ve-maps/ve.ui.MWMapsDialog.js
@@ -5,7 +5,7 @@
* @license The MIT License (MIT); see LICENSE.txt
*/
/* globals require */
-var kartoLive = require( 'ext.kartographer.live' ),
+var kartobox = require( 'ext.kartographer.box' ),
kartoEditing = require( 'ext.kartographer.editing' );
/**
@@ -22,6 +22,7 @@
ve.ui.MWMapsDialog.super.apply( this, arguments );
this.updateGeoJson = $.debounce( 300, $.proxy( this.updateGeoJson, this
) );
+ this.resetMapPosition = $.debounce( 300, $.proxy(
this.resetMapPosition, this ) );
};
/* Inheritance */
@@ -135,17 +136,20 @@
var position,
dialog = this;
- if ( this.map ) {
- position = this.getInitialMapPosition();
- this.map.setView( [ position.latitude, position.longitude ],
position.zoom );
- this.updateActions();
- this.resetMapButton.setDisabled( true );
-
- this.map.on( 'moveend', function () {
- dialog.updateActions();
- dialog.resetMapButton.setDisabled( false );
- } );
+ if ( !this.map ) {
+ return;
}
+
+ position = this.getInitialMapPosition();
+ this.map.setView( position.center, position.zoom );
+
+ this.updateActions();
+ this.resetMapButton.setDisabled( true );
+
+ this.map.once( 'moveend', function () {
+ dialog.updateActions();
+ dialog.resetMapButton.setDisabled( false );
+ } );
};
/**
@@ -282,16 +286,37 @@
mapPosition = dialog.getInitialMapPosition();
// TODO: Support 'style' editing
- dialog.MWMap = kartoLive.MWMap( dialog.$map[ 0 ], mapPosition );
- dialog.MWMap.ready( function ( map ) {
+ dialog.map = kartobox.map( {
+ container: dialog.$map[ 0 ],
+ center: mapPosition.center,
+ zoom: mapPosition.zoom
+ } );
- dialog.map = map;
+ dialog.map.doWhenReady( function () {
dialog.updateGeoJson();
dialog.onDimensionsChange();
dialog.resetMapPosition();
- geoJsonLayer = kartoEditing.getKartographerLayer( map );
+ // if geojson and no center, we need the map to
automatically
+ // position itself when the feature layer is added.
+ if ( dialog.input.getValue() && !mapPosition.center ) {
+ dialog.map.on( 'layeradd', function () {
+ var mwData = dialog.selectedNode &&
dialog.selectedNode.getAttribute( 'mw' ),
+ mwAttrs = mwData &&
mwData.attrs || {},
+ position;
+
+ dialog.map.setView( null,
mapPosition.zoom );
+ position = dialog.map.getMapPosition();
+
+ // update attributes with current
position
+ mwAttrs.latitude = position.center.lat;
+ mwAttrs.longitude = position.center.lng;
+ mwAttrs.zoom = position.zoom;
+ } );
+ }
+
+ geoJsonLayer = kartoEditing.getKartographerLayer(
dialog.map );
drawControl = new L.Control.Draw( {
edit: { featureGroup: geoJsonLayer },
draw: {
@@ -302,7 +327,7 @@
rectangle: defaultShapeOptions,
marker: { icon: L.mapbox.marker.icon(
{} ) }
}
- } ).addTo( map );
+ } ).addTo( dialog.map );
function update() {
// Prevent circular update of map
@@ -320,7 +345,7 @@
update();
}
- map
+ dialog.map
.on( 'draw:edited', update )
.on( 'draw:deleted', update )
.on( 'draw:created', created );
@@ -328,6 +353,37 @@
} );
} );
};
+/**
+ * Formats center if valid.
+ *
+ * @param {string|number} latitude
+ * @param {string|number} longitude
+ * @return {Array|undefined}
+ * @private
+ */
+function validCenter( latitude, longitude ) {
+ latitude = +latitude;
+ longitude = +longitude;
+
+ if ( !isNaN( latitude ) && !isNaN( longitude ) ) {
+ return [ latitude, longitude ];
+ }
+}
+
+/**
+ * Formats zoom if valid.
+ *
+ * @param {string|number} zoom
+ * @return {number|undefined}
+ * @private
+ */
+function validZoom( zoom ) {
+ zoom = +zoom;
+
+ if ( !isNaN( zoom ) ) {
+ return zoom;
+ }
+}
/**
* Get the initial map position (coordinates and zoom level)
@@ -335,22 +391,13 @@
* @return {Object} Object containing latitude, longitude and zoom
*/
ve.ui.MWMapsDialog.prototype.getInitialMapPosition = function () {
- var latitude, longitude, zoom,
- mwData = this.selectedNode && this.selectedNode.getAttribute(
'mw' ),
- mwAttrs = mwData && mwData.attrs;
+ var mwData = this.selectedNode && this.selectedNode.getAttribute( 'mw'
),
+ mwAttrs = mwData && mwData.attrs || {},
+ center = validCenter( mwAttrs.latitude, mwAttrs.longitude ),
+ zoom = validZoom( mwAttrs.zoom );
- if ( mwAttrs && mwAttrs.zoom ) {
- latitude = +mwAttrs.latitude;
- longitude = +mwAttrs.longitude;
- zoom = +mwAttrs.zoom;
- } else {
- latitude = 30;
- longitude = 0;
- zoom = 2;
- }
return {
- latitude: latitude,
- longitude: longitude,
+ center: center,
zoom: zoom
};
};
--
To view, visit https://gerrit.wikimedia.org/r/302286
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I00bbfcd15f608950f1110757a29dd1336f5a22e0
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Kartographer
Gerrit-Branch: master
Gerrit-Owner: JGirault <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits