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