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

Reply via email to