JGirault has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/295599

Change subject: Split the JS codebase into several modules.
......................................................................

Split the JS codebase into several modules.

Previously, the module/resource "ext.kartographer.live"
contained all the dependencies, and was loaded for both
MapLink and MapFrame tags.

With this refactor:
* MapLink tag only loads "ext.kartographer.maplink"
* MapFrame tag only loads "ext.kartographer.mapframe"
* These two resources define the dependencies for each
tag.
* The dependencies for displaying an interactive map
are contained in "ext.kartographer.live"
* The dependencies for displaying a map in full screen
mode are contained in "ext.kartographer.fullscreen"

Bug: T134079
Change-Id: Ifdeb529c86709ae0890d4445afce972fa26c9521
---
M extension.json
M includes/Tag/MapFrame.php
M includes/Tag/MapLink.php
A modules/fullscreen/CloseControl.js
A modules/fullscreen/MapDialog.js
A modules/fullscreen/fullscreen.js
D modules/kartographer.MapDialog.js
M modules/kartographer.js
A modules/live/FullScreenControl.js
A modules/live/live.js
A modules/mapframe/mapframe.js
A modules/maplink/maplink.js
A modules/settings/settings.js
13 files changed, 904 insertions(+), 762 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Kartographer 
refs/changes/99/295599/1

diff --git a/extension.json b/extension.json
index 9431ee6..321c337 100644
--- a/extension.json
+++ b/extension.json
@@ -129,18 +129,70 @@
                                "desktop"
                        ]
                },
-               "ext.kartographer.live": {
+               "ext.kartographer.init": {
                        "dependencies": [
-                               "mapbox",
                                "ext.kartographer",
-                               "ext.kartographer.site",
-                               "mediawiki.jqueryMsg",
-                               "oojs-ui.styles.icons-media",
+                               "mediawiki.jqueryMsg"
+                       ],
+                       "scripts": [
+                               "modules/kartographer.js"
+                       ],
+                       "targets": [
+                               "mobile",
+                               "desktop"
+                       ]
+               },
+               "ext.kartographer.maplink": {
+                       "dependencies": [
+                               "ext.kartographer.init",
                                "mediawiki.router"
                        ],
                        "scripts": [
+                               "modules/maplink/maplink.js"
+                       ],
+                       "targets": [
+                               "mobile",
+                               "desktop"
+                       ]
+               },
+               "ext.kartographer.settings": {
+                       "dependencies": [
+                               "mapbox"
+                       ],
+                       "scripts": [
+                               "modules/settings/settings.js"
+                       ],
+                       "targets": [
+                               "mobile",
+                               "desktop"
+                       ]
+               },
+               "ext.kartographer.mapframe": {
+                       "dependencies": [
+                               "mapbox",
+                               "ext.kartographer.init",
+                               "mediawiki.router",
+                               "ext.kartographer.live"
+                       ],
+                       "scripts": [
+                               "modules/mapframe/mapframe.js"
+                       ],
+                       "targets": [
+                               "mobile",
+                               "desktop"
+                       ]
+               },
+               "ext.kartographer.live": {
+                       "dependencies": [
+                               "mapbox",
+                               "ext.kartographer.settings",
+                               "mediawiki.router",
+                               "oojs-ui.styles.icons-media"
+                       ],
+                       "scripts": [
                                "lib/leaflet.sleep.js",
-                               "modules/kartographer.js"
+                               "modules/live/FullScreenControl.js",
+                               "modules/live/live.js"
                        ],
                        "messages": [
                                "kartographer-attribution"
@@ -152,11 +204,16 @@
                },
                "ext.kartographer.fullscreen": {
                        "dependencies": [
+                               "ext.kartographer.init",
                                "ext.kartographer.site",
+                               "ext.kartographer.live",
+                               "mediawiki.router",
                                "oojs-ui-windows"
                        ],
                        "scripts": [
-                               "modules/kartographer.MapDialog.js"
+                               "modules/fullscreen/fullscreen.js",
+                               "modules/fullscreen/CloseControl.js",
+                               "modules/fullscreen/MapDialog.js"
                        ],
                        "messages": [
                                "kartographer-fullscreen-close",
diff --git a/includes/Tag/MapFrame.php b/includes/Tag/MapFrame.php
index a7f1022..53d40e8 100644
--- a/includes/Tag/MapFrame.php
+++ b/includes/Tag/MapFrame.php
@@ -78,7 +78,7 @@
                        */
 
                        case 'interactive':
-                               $output->addModules( 'ext.kartographer.live' );
+                               $output->addModules( 
'ext.kartographer.mapframe' );
 
                                $width = is_numeric( $this->width ) ? 
"{$this->width}px" : $this->width;
                                $attrs = [
diff --git a/includes/Tag/MapLink.php b/includes/Tag/MapLink.php
index cab6e18..63ad0b5 100644
--- a/includes/Tag/MapLink.php
+++ b/includes/Tag/MapLink.php
@@ -21,7 +21,7 @@
 
        protected function render() {
                $output = $this->parser->getOutput();
-               $output->addModules( 'ext.kartographer.live' );
+               $output->addModules( 'ext.kartographer.maplink' );
                $interact = $output->getExtensionData( 'kartographer_interact' 
);
                if ( $interact === null ) {
                        $output->setExtensionData( 'kartographer_interact', [] 
);
diff --git a/modules/fullscreen/CloseControl.js 
b/modules/fullscreen/CloseControl.js
new file mode 100644
index 0000000..3ab008f
--- /dev/null
+++ b/modules/fullscreen/CloseControl.js
@@ -0,0 +1,28 @@
+/*jshint unused:false*/
+/**
+ * Close control on full screen mode.
+ */
+var 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
new file mode 100644
index 0000000..2ca02e8
--- /dev/null
+++ b/modules/fullscreen/MapDialog.js
@@ -0,0 +1,186 @@
+/* globals require, module */
+/* globals FullscreenCloseControl */
+( function ( $, mw, CloseControl ) {
+
+       var kartoLive = require( 'ext.kartographer.live' ),
+               MwKartographerMapDialog;
+
+       /**
+        * Dialog for full screen maps
+        *
+        * @class
+        * @extends OO.ui.Dialog
+        *
+        * @constructor
+        * @param {Object} [config] Configuration options
+        */
+       mw.kartographer.MapDialog = MwKartographerMapDialog = function () {
+               // Parent method
+               mw.kartographer.MapDialog.super.apply( this, arguments );
+       };
+
+       /* Inheritance */
+
+       OO.inheritClass( mw.kartographer.MapDialog, OO.ui.Dialog );
+
+       /* Static Properties */
+
+       mw.kartographer.MapDialog.static.size = 'full';
+
+       /* Methods */
+
+       mw.kartographer.MapDialog.prototype.initialize = function () {
+               // Parent method
+               mw.kartographer.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]
+        */
+       mw.kartographer.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.map.setView( new L.LatLng( extendedData.latitude, 
extendedData.longitude ), extendedData.zoom );
+                       this.map.mapData = mapData;
+                       this.movingMap = false;
+                       return;
+               }
+
+               this.setup.call( this, mapData );
+       };
+
+       mw.kartographer.MapDialog.prototype.getActionProcess = function ( 
action ) {
+               var dialog = this;
+               if ( !action ) {
+                       return new OO.ui.Process( function () {
+                               var router = mw.loader.require( 
'mediawiki.router' );
+                               if ( router.getPath() !== '' ) {
+                                       router.navigate( '' );
+                               } else {
+                                       // force close
+                                       dialog.close( { action: action } );
+                               }
+                       } );
+               }
+               return 
mw.kartographer.MapDialog.super.prototype.getActionProcess.call( this, action );
+       };
+
+       /**
+        * Tells the router to navigate to the current full screen map route.
+        */
+       mw.kartographer.MapDialog.prototype.updateHash = function () {
+               var router = mw.loader.require( 'mediawiki.router' ),
+                       hash = mw.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.
+        */
+       mw.kartographer.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 );
+
+       mw.kartographer.MapDialog.prototype.getSetupProcess = function ( 
mapData ) {
+               return 
mw.kartographer.MapDialog.super.prototype.getSetupProcess.call( this, mapData )
+                       .next( function () {
+                               var fullScreenState = mapData.fullScreenState,
+                                       extendedData = {};
+
+                               if ( this.map ) {
+                                       this.map.remove();
+                                       this.$map.remove();
+                               }
+
+                               this.$map = $( '<div>' )
+                                       .addClass( 
'mw-kartographer-mapDialog-map' )
+                                       .appendTo( this.$body );
+
+                               this.map = kartoLive.createMap( this.$map[ 0 ], 
mapData );
+                               this.map.addControl( new CloseControl( { 
dialog: this } ) );
+
+                               // copy of the initial settings
+                               this.mapData = mapData;
+
+                               if ( fullScreenState ) {
+                                       // override with full screen state
+                                       $.extend( extendedData, mapData, 
fullScreenState );
+                                       this.map.setView( new L.LatLng( 
extendedData.latitude, extendedData.longitude ), extendedData.zoom, true );
+                               }
+
+                               if ( typeof mapData.maptagId === 'number' ) {
+                                       this.map.on( 'moveend', this.onMapMove, 
this );
+                               }
+
+                               mw.hook( 'wikipage.maps' ).fire( this.map, true 
/* isFullScreen */ );
+                       }, this );
+       };
+
+       mw.kartographer.MapDialog.prototype.getReadyProcess = function ( data ) 
{
+               return 
mw.kartographer.MapDialog.super.prototype.getReadyProcess.call( this, data )
+                       .next( function () {
+                               this.map.invalidateSize();
+                       }, this );
+       };
+
+       mw.kartographer.MapDialog.prototype.getTeardownProcess = function ( 
data ) {
+               return 
mw.kartographer.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 );
+       };
+
+       module.exports = MwKartographerMapDialog;
+}( jQuery, mediaWiki, FullscreenCloseControl ) );
diff --git a/modules/fullscreen/fullscreen.js b/modules/fullscreen/fullscreen.js
new file mode 100644
index 0000000..15b4cbf
--- /dev/null
+++ b/modules/fullscreen/fullscreen.js
@@ -0,0 +1,78 @@
+/* globals require */
+( function ( $, mw ) {
+
+       var kartographer = require( 'ext.kartographer.init' ),
+               router = require( 'mediawiki.router' );
+
+       /**
+        * Get "editable" geojson layer for the map.
+        *
+        * If a layer doesn't exist, create and attach one.
+        *
+        * @param {L.mapbox.Map} map Map to get layers from
+        * @param {L.mapbox.FeatureLayer} map.kartographerLayer show 
tag-specific info in this layer
+        * @return {L.mapbox.FeatureLayer|null} GeoJSON layer, if present
+        */
+       mw.kartographer.getKartographerLayer = function ( map ) {
+               if ( !map.kartographerLayer ) {
+                       map.kartographerLayer = L.mapbox.featureLayer().addTo( 
map );
+               }
+               return map.kartographerLayer;
+       };
+
+       /**
+        * Updates "editable" GeoJSON layer from a string.
+        *
+        * Validates the GeoJSON against the `sanitize-mapdata` api
+        * before executing it.
+        *
+        * The deferred object will be resolved with a `boolean` flag
+        * indicating whether the GeoJSON was valid and was applied.
+        *
+        * @param {L.mapbox.Map} map Map to set the GeoJSON for
+        * @param {string} geoJsonString GeoJSON data, empty string to clear
+        * @return {jQuery.Promise} Promise which resolves when the GeoJSON is 
updated, and rejects if there was an error
+        */
+       mw.kartographer.updateKartographerLayer = function ( map, geoJsonString 
) {
+               var deferred = $.Deferred();
+
+               if ( geoJsonString === '' ) {
+                       return deferred.resolve().promise();
+               }
+
+               new mw.Api().post( {
+                       action: 'sanitize-mapdata',
+                       text: geoJsonString,
+                       title: mw.config.get( 'wgPageName' )
+               } ).done( function ( resp ) {
+                       var geoJson, layer,
+                               data = resp[ 'sanitize-mapdata' ];
+
+                       geoJsonString = data && data.sanitized;
+
+                       if ( geoJsonString && !data.error ) {
+                               try {
+                                       geoJson = JSON.parse( geoJsonString );
+                                       layer = 
mw.kartographer.getKartographerLayer( map );
+                                       layer.setGeoJSON( geoJson );
+                                       deferred.resolve();
+                               } catch ( e ) {
+                                       deferred.reject( e );
+                               }
+                       } else {
+                               deferred.reject();
+                       }
+               } );
+
+               return deferred.promise();
+       };
+
+       // Add index route.
+       router.route( '', function () {
+               // TODO: mapDialog is undefined
+               if ( kartographer.getMapDialog() ) {
+                       kartographer.getMapDialog().close();
+               }
+       } );
+
+}( jQuery, mediaWiki ) );
diff --git a/modules/kartographer.MapDialog.js 
b/modules/kartographer.MapDialog.js
deleted file mode 100644
index 2bb774c..0000000
--- a/modules/kartographer.MapDialog.js
+++ /dev/null
@@ -1,204 +0,0 @@
-/**
- * Dialog for full screen maps
- *
- * @class
- * @extends OO.ui.Dialog
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-mw.kartographer.MapDialog = function MwKartographerMapDialog() {
-       // Parent method
-       mw.kartographer.MapDialog.super.apply( this, arguments );
-};
-
-/* Inheritance */
-
-OO.inheritClass( mw.kartographer.MapDialog, OO.ui.Dialog );
-
-/* Static Properties */
-
-mw.kartographer.MapDialog.static.size = 'full';
-
-/* Methods */
-
-mw.kartographer.MapDialog.prototype.initialize = function () {
-       // Parent method
-       mw.kartographer.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]
- */
-mw.kartographer.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.map.setView( new L.LatLng( extendedData.latitude, 
extendedData.longitude ), extendedData.zoom );
-               this.map.mapData = mapData;
-               this.movingMap = false;
-               return;
-       }
-
-       this.setup.call( this, mapData );
-};
-
-mw.kartographer.MapDialog.prototype.getActionProcess = function ( action ) {
-       var dialog = this;
-       if ( !action ) {
-               return new OO.ui.Process( function () {
-                       var router = mw.loader.require( 'mediawiki.router' );
-                       if ( router.getPath() !== '' ) {
-                               router.navigate( '' );
-                       } else {
-                               // force close
-                               dialog.close( { action: action } );
-                       }
-               } );
-       }
-       return mw.kartographer.MapDialog.super.prototype.getActionProcess.call( 
this, action );
-};
-
-/**
- * Tells the router to navigate to the current full screen map route.
- */
-mw.kartographer.MapDialog.prototype.updateHash = function () {
-       var router = mw.loader.require( 'mediawiki.router' ),
-               hash = mw.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.
- */
-mw.kartographer.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 );
-
-mw.kartographer.MapDialog.prototype.getSetupProcess = function ( mapData ) {
-       return mw.kartographer.MapDialog.super.prototype.getSetupProcess.call( 
this, mapData )
-               .next( function () {
-                       var fullScreenState = mapData.fullScreenState,
-                               extendedData = {};
-
-                       if ( this.map ) {
-                               this.map.remove();
-                               this.$map.remove();
-                       }
-
-                       this.$map = $( '<div>' )
-                               .addClass( 'mw-kartographer-mapDialog-map' )
-                               .appendTo( this.$body );
-
-                       this.map = mw.kartographer.createMap( this.$map[ 0 ], 
mapData );
-                       this.map.addControl( new 
mw.kartographer.MapDialogCloseControl( { dialog: this } ) );
-
-                       // copy of the initial settings
-                       this.mapData = mapData;
-
-                       if ( fullScreenState ) {
-                               // override with full screen state
-                               $.extend( extendedData, mapData, 
fullScreenState );
-                               this.map.setView( new L.LatLng( 
extendedData.latitude, extendedData.longitude ), extendedData.zoom, true );
-                       }
-
-                       if ( typeof mapData.maptagId === 'number' ) {
-                               this.map.on( 'moveend', this.onMapMove, this );
-                       }
-
-                       mw.hook( 'wikipage.maps' ).fire( this.map, true /* 
isFullScreen */ );
-               }, this );
-};
-
-mw.kartographer.MapDialog.prototype.getReadyProcess = function ( data ) {
-       return mw.kartographer.MapDialog.super.prototype.getReadyProcess.call( 
this, data )
-               .next( function () {
-                       this.map.invalidateSize();
-               }, this );
-};
-
-mw.kartographer.MapDialog.prototype.getTeardownProcess = function ( data ) {
-       return 
mw.kartographer.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 );
-};
-
-/**
- * Close control on full screen mode.
- */
-mw.kartographer.MapDialogCloseControl = 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/kartographer.js b/modules/kartographer.js
index fca4daf..f3df0fa 100644
--- a/modules/kartographer.js
+++ b/modules/kartographer.js
@@ -1,333 +1,16 @@
+/* globals module */
 ( function ( $, mw ) {
 
-       // Load this script after lib/mapbox-lib.js
-       var scale, urlFormat, windowManager, mapDialog,
-               mapServer = mw.config.get( 'wgKartographerMapServer' ),
-               forceHttps = mapServer[ 4 ] === 's',
-               config = L.mapbox.config,
-               router = mw.loader.require( 'mediawiki.router' ),
-               worldLatLng = new L.LatLngBounds( [ -90, -180 ], [ 90, 180 ] );
+       var windowManager, mapDialog;
 
-       config.REQUIRE_ACCESS_TOKEN = false;
-       config.FORCE_HTTPS = forceHttps;
-       config.HTTP_URL = forceHttps ? false : mapServer;
-       config.HTTPS_URL = !forceHttps ? false : mapServer;
-
-       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';
-
-       mw.kartographer = {};
-
-       /**
-        * References the map containers of the page.
-        *
-        * @type {HTMLElement[]}
-        */
-       mw.kartographer.maps = [];
-
-       /**
-        * References the maplinks of the page.
-        *
-        * @type {HTMLElement[]}
-        */
-       mw.kartographer.maplinks = [];
-
-       mw.kartographer.FullScreenControl = 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' 
);
-
-                       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 = mw.kartographer.getMapHash( 
this.options.mapData, this.map );
-                       this.link.href = '#' + hash;
-               },
-
-               onShowFullScreen: function ( e ) {
-                       L.DomEvent.stop( e );
-                       mw.kartographer.openFullscreenMap( this.map, 
getMapPosition( this.map ) );
-               }
-       } );
-       /**
-        * 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;
-       }
-
-       /**
-        * Create a new interactive map
-        *
-        * @param {HTMLElement} container Map container
-        * @param {Object} data Map data
-        * @param {number} data.latitude Latitude
-        * @param {number} data.longitude Longitude
-        * @param {number} data.zoom Zoom
-        * @param {string} [data.style] Map style
-        * @param {string[]} [data.overlays] Names of overlay groups to show
-        * @param {boolean} [data.enableFullScreenButton] add zoom
-        * @return {L.mapbox.Map} Map object
-        */
-       mw.kartographer.createMap = function ( container, data ) {
-               var map,
-                       $container = $( container ),
-                       style = data.style || mw.config.get( 
'wgKartographerDfltStyle' ),
-                       width, height,
-                       maxBounds;
-
-               $container.addClass( 'mw-kartographer-map' );
-
-               map = L.map( container );
-
-               if ( !container.clientWidth ) {
-                       // Get `max` properties in case the container was 
wrapped
-                       // with {@link #responsiveContainerWrap}.
-                       width = $container.css( 'max-width' );
-                       height = $container.css( 'max-height' );
-                       width = ( !width || width === 'none' ) ? 
$container.width() : width;
-                       height = ( !height || height === 'none' ) ? 
$container.height() : height;
-
-                       // HACK: If the container is not naturally measurable, 
try jQuery
-                       // which will pick up CSS dimensions. T125263
-                       /*jscs:disable disallowDanglingUnderscores */
-                       map._size = new L.Point( width, height );
-                       /*jscs:enable disallowDanglingUnderscores */
-               }
-
-               /**
-                * @property {L.TileLayer} Reference to `Wikimedia` tile layer.
-                */
-               map.wikimediaLayer = L.tileLayer( mapServer + '/' + style + 
urlFormat, {
-                       maxZoom: 18,
-                       attribution: mw.message( 'kartographer-attribution' 
).parse()
-               } ).addTo( map );
-
-               /**
-                * @property {Object} Hash map of data groups and their 
corresponding
-                *   {@link L.mapbox.FeatureLayer layers}.
-                */
-               map.dataLayers = {};
-
-               if ( data.overlays ) {
-
-                       getMapGroupData( data.overlays ).done( function ( 
mapData ) {
-                               $.each( data.overlays, function ( index, group 
) {
-                                       if ( !$.isEmptyObject( mapData[ group ] 
) ) {
-                                               map.dataLayers[ group ] = 
mw.kartographer.addDataLayer( map, mapData[ group ] );
-                                       } else {
-                                               mw.log.warn( 'Layer not found 
or contains no data: "' + group + '"' );
-                                       }
-                               } );
-                       } );
-
-               }
-
-               // Position the map
-               if ( isNaN( data.longitude ) && isNaN( data.latitude ) ) {
-                       // 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 );
-                       }
-                       // 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
-                       } );
-               } else {
-                       map.setView( [ data.latitude, data.longitude ], 
data.zoom, true );
-               }
-
-               map.attributionControl.setPrefix( '' );
-
-               if ( data.enableFullScreenButton ) {
-                       map.addControl( new mw.kartographer.FullScreenControl( {
-                               mapData: data
-                       } ) );
-               }
-
-               return map;
-       };
-
-       mw.kartographer.dataLayerOpts = {
-               // Disable double-sanitization by mapbox's internal sanitizer
-               // because geojson has already passed through the MW internal 
sanitizer
-               sanitizer: function ( v ) {
-                       return v;
-               }
-       };
-
-       /**
-        * Create a new GeoJSON layer and add it to map.
-        *
-        * @param {L.mapbox.Map} map Map to get layers from
-        * @param {Object} geoJson
-        */
-       mw.kartographer.addDataLayer = function ( map, geoJson ) {
-               try {
-                       return L.mapbox.featureLayer( geoJson, 
mw.kartographer.dataLayerOpts ).addTo( map );
-               } catch ( e ) {
-                       mw.log( e );
-               }
-       };
-
-       /**
-        * Get "editable" geojson layer for the map.
-        *
-        * If a layer doesn't exist, create and attach one.
-        *
-        * @param {L.mapbox.Map} map Map to get layers from
-        * @param {L.mapbox.FeatureLayer} map.kartographerLayer show 
tag-specific info in this layer
-        * @return {L.mapbox.FeatureLayer|null} GeoJSON layer, if present
-        */
-       mw.kartographer.getKartographerLayer = function ( map ) {
-               if ( !map.kartographerLayer ) {
-                       map.kartographerLayer = L.mapbox.featureLayer().addTo( 
map );
-               }
-               return map.kartographerLayer;
-       };
-
-       /**
-        * Updates "editable" GeoJSON layer from a string.
-        *
-        * Validates the GeoJSON against the `sanitize-mapdata` api
-        * before executing it.
-        *
-        * The deferred object will be resolved with a `boolean` flag
-        * indicating whether the GeoJSON was valid and was applied.
-        *
-        * @param {L.mapbox.Map} map Map to set the GeoJSON for
-        * @param {string} geoJsonString GeoJSON data, empty string to clear
-        * @return {jQuery.Promise} Promise which resolves when the GeoJSON is 
updated, and rejects if there was an error
-        */
-       mw.kartographer.updateKartographerLayer = function ( map, geoJsonString 
) {
-               var deferred = $.Deferred();
-
-               if ( geoJsonString === '' ) {
-                       return deferred.resolve().promise();
-               }
-
-               new mw.Api().post( {
-                       action: 'sanitize-mapdata',
-                       text: geoJsonString,
-                       title: mw.config.get( 'wgPageName' )
-               } ).done( function ( resp ) {
-                       var geoJson, layer,
-                               data = resp[ 'sanitize-mapdata' ];
-
-                       geoJsonString = data && data.sanitized;
-
-                       if ( geoJsonString && !data.error ) {
-                               try {
-                                       geoJson = JSON.parse( geoJsonString );
-                                       layer = 
mw.kartographer.getKartographerLayer( map );
-                                       layer.setGeoJSON( geoJson );
-                                       deferred.resolve();
-                               } catch ( e ) {
-                                       deferred.reject( e );
-                               }
-                       } else {
-                               deferred.reject();
-                       }
-               } );
-
-               return deferred.promise();
-       };
+       mw.kartographer = mw.kartographer || {};
 
        function getWindowManager() {
                if ( !windowManager ) {
                        windowManager = new OO.ui.WindowManager();
-                       mapDialog = new mw.kartographer.MapDialog();
+                       setMapDialog( new mw.kartographer.MapDialog() );
                        $( 'body' ).append( windowManager.$element );
-                       windowManager.addWindows( [ mapDialog ] );
+                       windowManager.addWindows( [ getMapDialog() ] );
                }
                return windowManager;
        }
@@ -374,21 +57,25 @@
                                        enableFullScreenButton: false
                                } );
 
-                               if ( mapDialog ) {
-                                       mapDialog.changeMap( dialogData );
+                               if ( getMapDialog() ) {
+                                       getMapDialog().changeMap( dialogData );
                                        return;
                                }
                                getWindowManager()
-                                       .openWindow( mapDialog, dialogData )
-                                       .then( function ( opened ) { return 
opened; } )
+                                       .openWindow( getMapDialog(), dialogData 
)
+                                       .then( function ( opened ) {
+                                               return opened;
+                                       } )
                                        .then( function ( closing ) {
+                                               var dialog = getMapDialog();
                                                if ( map ) {
                                                        map.setView(
-                                                               
mapDialog.map.getCenter(),
-                                                               
mapDialog.map.getZoom()
+                                                               
dialog.map.getCenter(),
+                                                               
dialog.map.getZoom()
                                                        );
                                                }
-                                               windowManager = mapDialog = 
null;
+                                               setMapDialog( null );
+                                               windowManager = null;
                                                return closing;
                                        } );
                        } );
@@ -521,229 +208,22 @@
                return obj;
        }
 
-       /**
-        * 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 = [];
-
-               $( overlays ).each( function ( key, value ) {
-                       if ( !( value in groupsLoaded ) ) {
-                               groupsToLoad.push( value );
-                       }
-               } );
-
-               if ( !groupsToLoad.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 );
-                       mw.config.set( 'wgKartographerLiveData', groupsLoaded );
-
-                       deferred.resolve( groupsLoaded );
-               } );
-
-               return deferred.promise();
+       function getMapDialog() {
+               return mapDialog;
        }
 
-       /**
-        * 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.
-        */
-       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;
-
-               // 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 = $( '<div>' );
-               $container.append( $responder.append( $map ) );
-               return $map[ 0 ];
+       function setMapDialog( dialog ) {
+               mapDialog = dialog;
+               return mapDialog;
        }
 
-       /**
-        * This code will be executed once the article is rendered and ready.
-        */
-       mw.hook( 'wikipage.content' ).add( function ( $content ) {
-               var mapsInArticle = [],
-                       isMobile = mw.config.get( 'skin' ) === 'minerva';
+       module.exports = {
+               getMapHash: mw.kartographer.getMapHash,
+               openFullscreenMap: mw.kartographer.openFullscreenMap,
+               getMapData: getMapData,
+               getMapPosition: getMapPosition,
+               getFullScreenState: getFullScreenState,
+               getMapDialog: getMapDialog
+       };
 
-               // 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 
) {
-                       mw.kartographer.maplinks[ index ] = this;
-
-                       $( this ).data( 'maptag-id', index );
-                       this.href = '#' + '/maplink/' + index;
-               } );
-
-               L.Map.mergeOptions( {
-                       sleepTime: 250,
-                       wakeTime: 1000,
-                       sleepNote: false,
-                       sleepOpacity: 1
-               } );
-
-               $content.find( '.mw-kartographer-interactive' ).each( function 
( index ) {
-                       var map, data,
-                               container = this,
-                               $container = $( this );
-
-                       $container.data( 'maptag-id', index );
-                       data = getMapData( container );
-
-                       if ( data ) {
-                               data.enableFullScreenButton = true;
-
-                               if ( isMobile ) {
-                                       container = responsiveContainerWrap( 
container );
-                               }
-
-                               map = mw.kartographer.createMap( container, 
data );
-                               map.doubleClickZoom.disable();
-
-                               mapsInArticle.push( map );
-                               mw.kartographer.maps[ index ] = map;
-
-                               $container.on( 'dblclick', function () {
-                                       if ( router.isSupported() ) {
-                                               router.navigate( 
mw.kartographer.getMapHash( data, map ) );
-                                       } else {
-                                               
mw.kartographer.openFullscreenMap( map, getMapPosition( map ) );
-                                       }
-                               } );
-
-                               // 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`.
-                               if ( !$container.is( ':visible' ) ) {
-                                       $container.closest( '.mw-collapsible' )
-                                               .on( 
'afterExpand.mw-collapsible', function () {
-                                                       map.invalidateSize();
-                                               } );
-                               }
-                       }
-               } );
-
-               // Allow customizations of interactive maps in article.
-               mw.hook( 'wikipage.maps' ).fire( mapsInArticle, false /* 
isFullScreen */ );
-
-               // Opens a map in full screen. 
#/map(/:zoom)(/:latitude)(/:longitude)
-               // Examples:
-               //     #/map/0
-               //     #/map/0/5
-               //     #/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 = mw.kartographer.maps[ maptagId ];
-                       if ( !map ) {
-                               router.navigate( '' );
-                               return;
-                       }
-                       mw.kartographer.openFullscreenMap( map, 
getFullScreenState( zoom, latitude, longitude ) );
-               } );
-
-               // Opens a maplink in full screen. 
#/maplink(/:zoom)(/:latitude)(/:longitude)
-               // Examples:
-               //     #/maplink/0
-               //     #/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 = mw.kartographer.maplinks[ maptagId ],
-                               data;
-
-                       if ( !link ) {
-                               router.navigate( '' );
-                               return;
-                       }
-                       data = getMapData( link );
-                       mw.kartographer.openFullscreenMap( data, 
getFullScreenState( zoom, latitude, longitude ) );
-               } );
-
-               // Check if we need to open a map in full screen.
-               router.checkRoute();
-
-               // Add index route.
-               router.route( '', function () {
-                       if ( mapDialog ) {
-                               mapDialog.close();
-                       }
-               } );
-       } );
 }( jQuery, mediaWiki ) );
diff --git a/modules/live/FullScreenControl.js 
b/modules/live/FullScreenControl.js
new file mode 100644
index 0000000..f2bcf9c
--- /dev/null
+++ b/modules/live/FullScreenControl.js
@@ -0,0 +1,46 @@
+var router = mw.loader.require( 'mediawiki.router' ),
+       kartographer = mw.loader.require( 'ext.kartographer.init' ),
+       LiveFullScreenControl;
+
+LiveFullScreenControl = 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' );
+
+               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 = mw.kartographer.getMapHash( this.options.mapData, 
this.map );
+               this.link.href = '#' + hash;
+       },
+
+       onShowFullScreen: function ( e ) {
+               L.DomEvent.stop( e );
+               mw.kartographer.openFullscreenMap( this.map, 
kartographer.getMapPosition( this.map ) );
+       }
+} );
diff --git a/modules/live/live.js b/modules/live/live.js
new file mode 100644
index 0000000..4aee580
--- /dev/null
+++ b/modules/live/live.js
@@ -0,0 +1,255 @@
+/* globals module */
+/* globals LiveFullScreenControl */
+( function ( $, mw, FullScreenControl ) {
+
+       var scale, urlFormat,
+               mapServer = mw.config.get( 'wgKartographerMapServer' ),
+               createMap,
+               worldLatLng = new L.LatLngBounds( [ -90, -180 ], [ 90, 180 ] );
+
+       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
+       } );
+
+       /**
+        * Create a new interactive map
+        *
+        * @param {HTMLElement} container Map container
+        * @param {Object} data Map data
+        * @param {number} data.latitude Latitude
+        * @param {number} data.longitude Longitude
+        * @param {number} data.zoom Zoom
+        * @param {string} [data.style] Map style
+        * @param {string[]} [data.overlays] Names of overlay groups to show
+        * @param {boolean} [data.enableFullScreenButton] add zoom
+        * @return {L.mapbox.Map} Map object
+        */
+       createMap = function ( container, data ) {
+               var map,
+                       $container = $( container ),
+                       style = data.style || mw.config.get( 
'wgKartographerDfltStyle' ),
+                       width, height,
+                       maxBounds;
+
+               $container.addClass( 'mw-kartographer-map' );
+
+               map = L.map( container );
+
+               if ( !container.clientWidth ) {
+                       // Get `max` properties in case the container was 
wrapped
+                       // with {@link #responsiveContainerWrap}.
+                       width = $container.css( 'max-width' );
+                       height = $container.css( 'max-height' );
+                       width = ( !width || width === 'none' ) ? 
$container.width() : width;
+                       height = ( !height || height === 'none' ) ? 
$container.height() : height;
+
+                       // HACK: If the container is not naturally measurable, 
try jQuery
+                       // which will pick up CSS dimensions. T125263
+                       /*jscs:disable disallowDanglingUnderscores */
+                       map._size = new L.Point( width, height );
+                       /*jscs:enable disallowDanglingUnderscores */
+               }
+
+               /**
+                * @property {L.TileLayer} Reference to `Wikimedia` tile layer.
+                */
+               map.wikimediaLayer = L.tileLayer( mapServer + '/' + style + 
urlFormat, {
+                       maxZoom: 18,
+                       attribution: mw.message( 'kartographer-attribution' 
).parse()
+               } ).addTo( map );
+
+               /**
+                * @property {Object} Hash map of data groups and their 
corresponding
+                *   {@link L.mapbox.FeatureLayer layers}.
+                */
+               map.dataLayers = {};
+
+               if ( data.overlays ) {
+
+                       getMapGroupData( data.overlays ).done( function ( 
mapData ) {
+                               $.each( data.overlays, function ( index, group 
) {
+                                       if ( !$.isEmptyObject( mapData[ group ] 
) ) {
+                                               map.dataLayers[ group ] = 
mw.kartographer.addDataLayer( map, mapData[ group ] );
+                                       } else {
+                                               mw.log.warn( 'Layer not found 
or contains no data: "' + group + '"' );
+                                       }
+                               } );
+                       } );
+
+               }
+
+               // Position the map
+               if ( isNaN( data.longitude ) && isNaN( data.latitude ) ) {
+                       // 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 );
+                       }
+                       // 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
+                       } );
+               } else {
+                       map.setView( [ data.latitude, data.longitude ], 
data.zoom, true );
+               }
+
+               map.attributionControl.setPrefix( '' );
+
+               if ( data.enableFullScreenButton ) {
+                       map.addControl( new FullScreenControl( {
+                               mapData: data
+                       } ) );
+               }
+
+               return map;
+       };
+
+       mw.kartographer.dataLayerOpts = {
+               // Disable double-sanitization by mapbox's internal sanitizer
+               // because geojson has already passed through the MW internal 
sanitizer
+               sanitizer: function ( v ) {
+                       return v;
+               }
+       };
+
+       /**
+        * Create a new GeoJSON layer and add it to map.
+        *
+        * @param {L.mapbox.Map} map Map to get layers from
+        * @param {Object} geoJson
+        */
+       mw.kartographer.addDataLayer = function ( map, geoJson ) {
+               try {
+                       return L.mapbox.featureLayer( geoJson, 
mw.kartographer.dataLayerOpts ).addTo( map );
+               } catch ( e ) {
+                       mw.log( e );
+               }
+       };
+
+       /**
+        * 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 = [];
+
+               $( overlays ).each( function ( key, value ) {
+                       if ( !( value in groupsLoaded ) ) {
+                               groupsToLoad.push( value );
+                       }
+               } );
+
+               if ( !groupsToLoad.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 );
+                       mw.config.set( 'wgKartographerLiveData', groupsLoaded );
+
+                       deferred.resolve( groupsLoaded );
+               } );
+
+               return deferred.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;
+       }
+
+       module.exports = {
+               FullScreenControl: FullScreenControl,
+               createMap: createMap
+       };
+
+}( jQuery, mediaWiki, LiveFullScreenControl ) );
diff --git a/modules/mapframe/mapframe.js b/modules/mapframe/mapframe.js
new file mode 100644
index 0000000..b41adb2
--- /dev/null
+++ b/modules/mapframe/mapframe.js
@@ -0,0 +1,155 @@
+/* globals require */
+( function ( $, mw ) {
+
+       var router = require( 'mediawiki.router' ),
+               kartographer = require( 'ext.kartographer.init' ),
+               kartoLive = require( 'ext.kartographer.live' );
+
+       /**
+        * References the map containers of the page.
+        *
+        * @type {HTMLElement[]}
+        */
+       mw.kartographer.maps = [];
+
+       /**
+        * 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.
+        */
+       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;
+
+               // 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 = $( '<div>' );
+               $container.append( $responder.append( $map ) );
+               return $map[ 0 ];
+       }
+
+       /**
+        * This code will be executed once the article is rendered and ready.
+        */
+       mw.hook( 'wikipage.content' ).add( function ( $content ) {
+               var mapsInArticle = [],
+                       isMobile = mw.config.get( 'skin' ) === 'minerva';
+
+               $content.find( '.mw-kartographer-interactive' ).each( function 
( index ) {
+                       var map, data,
+                               container = this,
+                               $container = $( this );
+
+                       $container.data( 'maptag-id', index );
+                       data = kartographer.getMapData( container );
+
+                       if ( data ) {
+                               data.enableFullScreenButton = true;
+
+                               if ( isMobile ) {
+                                       container = responsiveContainerWrap( 
container );
+                               }
+
+                               map = kartoLive.createMap( container, data );
+                               map.doubleClickZoom.disable();
+
+                               mapsInArticle.push( map );
+                               mw.kartographer.maps[ index ] = map;
+
+                               $container.on( 'dblclick', function () {
+                                       if ( router.isSupported() ) {
+                                               router.navigate( 
kartographer.getMapHash( data, map ) );
+                                       } else {
+                                               kartographer.openFullscreenMap( 
map, kartographer.getMapPosition( map ) );
+                                       }
+                               } );
+
+                               // 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`.
+                               if ( !$container.is( ':visible' ) ) {
+                                       $container.closest( '.mw-collapsible' )
+                                               .on( 
'afterExpand.mw-collapsible', function () {
+                                                       map.invalidateSize();
+                                               } );
+                               }
+                       }
+               } );
+
+               // Allow customizations of interactive maps in article.
+               mw.hook( 'wikipage.maps' ).fire( mapsInArticle, false /* 
isFullScreen */ );
+
+               // Opens a map in full screen. 
#/map(/:zoom)(/:latitude)(/:longitude)
+               // Examples:
+               //     #/map/0
+               //     #/map/0/5
+               //     #/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 = mw.kartographer.maps[ maptagId ];
+                       if ( !map ) {
+                               router.navigate( '' );
+                               return;
+                       }
+
+                       mw.kartographer.openFullscreenMap( map, 
kartographer.getFullScreenState( zoom, latitude, longitude ) );
+               } );
+
+               // Check if we need to open a map in full screen.
+               router.checkRoute();
+       } );
+}( jQuery, mediaWiki ) );
diff --git a/modules/maplink/maplink.js b/modules/maplink/maplink.js
new file mode 100644
index 0000000..a3395c4
--- /dev/null
+++ b/modules/maplink/maplink.js
@@ -0,0 +1,49 @@
+/* globals require */
+( function ( $, mw ) {
+
+       var kartographer = require( 'ext.kartographer.init' ),
+               router = require( 'mediawiki.router' );
+
+       /**
+        * References the maplinks of the page.
+        *
+        * @type {HTMLElement[]}
+        */
+       mw.kartographer.maplinks = [];
+
+       /**
+        * This code will be executed once the article is rendered and ready.
+        */
+       mw.hook( 'wikipage.content' ).add( function ( ) {
+
+               // 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 
) {
+                       mw.kartographer.maplinks[ index ] = this;
+
+                       $( this ).data( 'maptag-id', index );
+                       this.href = '#' + '/maplink/' + index;
+               } );
+
+               // Opens a maplink in full screen. 
#/maplink(/:zoom)(/:latitude)(/:longitude)
+               // Examples:
+               //     #/maplink/0
+               //     #/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 = mw.kartographer.maplinks[ maptagId ],
+                               data;
+
+                       if ( !link ) {
+                               router.navigate( '' );
+                               return;
+                       }
+                       data = kartographer.getMapData( link );
+                       mw.kartographer.openFullscreenMap( data, 
kartographer.getFullScreenState( zoom, latitude, longitude ) );
+               } );
+
+               // Check if we need to open a map in full screen.
+               router.checkRoute();
+       } );
+}( jQuery, mediaWiki ) );
diff --git a/modules/settings/settings.js b/modules/settings/settings.js
new file mode 100644
index 0000000..f97f220
--- /dev/null
+++ b/modules/settings/settings.js
@@ -0,0 +1,12 @@
+( function ( $, mw ) {
+
+       var mapServer = mw.config.get( 'wgKartographerMapServer' ),
+               forceHttps = mapServer[ 4 ] === 's',
+               config = L.mapbox.config;
+
+       config.REQUIRE_ACCESS_TOKEN = false;
+       config.FORCE_HTTPS = forceHttps;
+       config.HTTP_URL = forceHttps ? false : mapServer;
+       config.HTTPS_URL = !forceHttps ? false : mapServer;
+
+}( jQuery, mediaWiki ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/295599
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ifdeb529c86709ae0890d4445afce972fa26c9521
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Kartographer
Gerrit-Branch: master
Gerrit-Owner: JGirault <julien.inbox.w...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to