MediaWiki:DataMaps.js: Difference between revisions
Jump to navigation
Jump to search
Labels: tag by group, hide minor-group labels when zoomed far out |
Label-only groups: wilds show as clickable italic region names with no icon |
||
| (6 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/** | /** | ||
* Interactive maps (Extension:DataMaps) site script. | * Interactive maps (Extension:DataMaps) site script. | ||
* Binds a permanent name label to every marker so the maps read like the printed ones, | * - Binds a permanent name label to every marker so the maps read like the printed ones, | ||
* | * hiding labels of the busier groups while zoomed far out. | ||
* Styling lives in MediaWiki:DataMaps.css (. | * - "Label-only" groups (LABEL_ONLY_GROUPS, or a map's `custom.labelOnlyGroups`): the icon is | ||
* hidden and the name is drawn centred on the spot as an italic region name; clicking the | |||
* name opens the marker's article. The marker itself still filters, searches and pops up. | |||
* - Draws clickable, labelled region outlines from a map's `custom.regions` | |||
* ([{ name, article, points: [[x,y], …] }]) — used by Map:Faerun to open the regional maps. | |||
* - Applies a starting view (`custom.initialZoomIn` = number of zoom-button steps past the fitted | |||
* view; `custom.initialCenter` = [x, y]) and unticks `custom.hiddenGroups` in the legend on load. | |||
* Styling lives in MediaWiki:DataMaps.css. | |||
* | |||
* Label placement: by group (DIRECTION), overridable per marker by ending its id with | |||
* --l, --r, --t or --b. A map can widen the set of "minor" (hidden-when-far) groups with | |||
* `custom.labelMinorGroups`. | |||
*/ | */ | ||
mw.loader.using( 'ext.datamaps.bootstrap' ).then( function () { | mw.loader.using( [ 'ext.datamaps.bootstrap', 'ext.datamaps.leaflet', 'mediawiki.util' ] ).then( function ( require ) { | ||
var MINOR_GROUPS = [ 'town', 'ruin', 'wilds', ' | var Leaflet = require( 'ext.datamaps.leaflet' ), | ||
MINOR_GROUPS = [ 'town', 'ruin', 'rumour' ], | |||
LABEL_ONLY_GROUPS = [ 'wilds' ], | |||
DIRECTION = { stronghold: 'bottom', skarhald: 'bottom', party: 'top' }, | |||
SUFFIX = { l: 'left', r: 'right', t: 'top', b: 'bottom' }, | |||
ZOOM_STEP = 0.25, // one click of the zoom buttons | |||
// Minor labels appear once the map is zoomed in this many levels past its fitted (initial) | |||
// zoom. 0.45 = two clicks. | |||
NEAR_DELTA = 0.45; | |||
function hideIcon( marker ) { | |||
var el = marker.getElement && marker.getElement(); | |||
if ( el ) { | |||
el.classList.add( 'dn-icon-hidden' ); | |||
} | |||
} | |||
mw.dataMaps.registerMapAddedHandler( function ( map ) { | mw.dataMaps.registerMapAddedHandler( function ( map ) { | ||
var isMini = map.rootElement.classList.contains( 'ext-datamaps-mini-layout' ); | var isMini = map.rootElement.classList.contains( 'ext-datamaps-mini-layout' ), | ||
custom = map.config.custom || {}, | |||
minorGroups = custom.labelMinorGroups || MINOR_GROUPS, | |||
labelOnlyGroups = custom.labelOnlyGroups || LABEL_ONLY_GROUPS; | |||
map.on( 'markerReady', function ( marker ) { | map.on( 'markerReady', function ( marker ) { | ||
var state = marker.apiInstance && marker.apiInstance[ 2 ], | var state = marker.apiInstance && marker.apiInstance[ 2 ], | ||
group = marker.attachedLayers && marker.attachedLayers[ 0 ], | group = marker.attachedLayers && marker.attachedLayers[ 0 ], | ||
size, | uid = state && state.uid ? String( state.uid ) : '', | ||
suffix = /--([lrtb])$/.exec( uid ), | |||
labelOnly = labelOnlyGroups.indexOf( group ) !== -1, | |||
direction = labelOnly ? 'center' : ( ( suffix && SUFFIX[ suffix[ 1 ] ] ) || DIRECTION[ group ] || 'right' ), | |||
size, half, offset, cls; | |||
if ( !state || !state.label || typeof marker.bindTooltip !== 'function' ) { | if ( !state || !state.label || typeof marker.bindTooltip !== 'function' ) { | ||
return; | return; | ||
} | } | ||
// | // Icons are anchored at their centre; push the label just clear of the icon edge. | ||
size = marker.options && marker.options.icon && marker.options.icon.options.iconSize; | size = marker.options && marker.options.icon && marker.options.icon.options.iconSize; | ||
half = ( size ? ( Array.isArray( size ) ? size[ 0 ] : size.x || 40 ) : 24 ) / 2; | |||
cls = 'dn-map-label dn-map-label-g-' + ( group || 'none' ); | offset = direction === 'right' ? [ half + 2, 0 ] : | ||
direction === 'left' ? [ -( half + 2 ), 0 ] : | |||
direction === 'top' ? [ 0, -( half - 2 ) ] : | |||
direction === 'bottom' ? [ 0, half - 2 ] : [ 0, 0 ]; | |||
cls = 'dn-map-label dn-map-label-' + direction + ' dn-map-label-g-' + ( group || 'none' ); | |||
if ( isMini ) { | if ( isMini ) { | ||
cls += ' dn-map-label-mini'; | cls += ' dn-map-label-mini'; | ||
} | } | ||
if ( | if ( minorGroups.indexOf( group ) !== -1 ) { | ||
cls += ' dn-map-label-minor'; | cls += ' dn-map-label-minor'; | ||
} | |||
if ( labelOnly ) { | |||
cls += ' dn-map-label-only' + ( state.article ? ' dn-map-label-link' : '' ); | |||
hideIcon( marker ); | |||
marker.on( 'add', function () { hideIcon( marker ); } ); | |||
if ( state.article ) { | |||
marker.on( 'tooltipopen', function ( e ) { | |||
var el = e.tooltip && e.tooltip.getElement && e.tooltip.getElement(); | |||
if ( el && !el.dataset.dnLinked ) { | |||
el.dataset.dnLinked = '1'; | |||
el.title = state.article; | |||
el.addEventListener( 'click', function ( ev ) { | |||
ev.stopPropagation(); | |||
window.location.href = mw.util.getUrl( state.article ); | |||
} ); | |||
} | |||
} ); | |||
} | |||
} | } | ||
marker.bindTooltip( state.label, { | marker.bindTooltip( state.label, { | ||
permanent: true, | permanent: true, | ||
direction: | direction: direction, | ||
offset: | offset: offset, | ||
interactive: | interactive: labelOnly, | ||
opacity: 1, | opacity: 1, | ||
className: cls | className: cls | ||
| Line 39: | Line 93: | ||
} ); | } ); | ||
// | // Groups that start unticked in the legend on this map (marker data still loads, so the | ||
// reader can switch them on). | |||
if ( custom.hiddenGroups && !isMini ) { | |||
map.on( 'markerFilteringPanel', function () { | |||
custom.hiddenGroups.forEach( function ( groupId ) { | |||
var row = map.filtersPanel && map.filtersPanel.groupToggles[ groupId ]; | |||
if ( row ) { | |||
row.checkbox.setSelected( false ); | |||
} | |||
} ); | |||
} ); | |||
} | |||
map.on( 'leafletLoaded', function () { | map.on( 'leafletLoaded', function () { | ||
var leafletMap = map.viewport && map.viewport.getLeafletMap(); | var leafletMap = map.viewport && map.viewport.getLeafletMap(), | ||
baseZoom = null; | |||
if ( !leafletMap ) { | if ( !leafletMap ) { | ||
return; | return; | ||
} | } | ||
// Zoom tier → data attribute the CSS keys off. "far" = at or near the fitted zoom. | |||
function updateTier() { | function updateTier() { | ||
map.rootElement.dataset.dnLabelTier = | var z = leafletMap.getZoom(); | ||
if ( baseZoom === null || z < baseZoom ) { | |||
baseZoom = z; // the most zoomed-out level seen is the fitted view | |||
} | |||
map.rootElement.dataset.dnZoom = z.toFixed( 2 ); | |||
map.rootElement.dataset.dnBaseZoom = baseZoom.toFixed( 2 ); | |||
map.rootElement.dataset.dnLabelTier = ( z - baseZoom ) < NEAR_DELTA ? 'far' : 'near'; | |||
} | } | ||
leafletMap.on( 'zoomend', updateTier ); | leafletMap.on( 'zoomend', updateTier ); | ||
updateTier(); | leafletMap.whenReady( updateTier ); | ||
// Starting view: zoom in a few steps from the fitted view, optionally around a point. | |||
if ( !isMini && ( custom.initialZoomIn || custom.initialCenter ) ) { | |||
leafletMap.whenReady( function () { | |||
var zoom = leafletMap.getZoom() + ( custom.initialZoomIn || 0 ) * ZOOM_STEP, | |||
center = custom.initialCenter ? | |||
map.crs.fromPoint( [ custom.initialCenter[ 1 ], custom.initialCenter[ 0 ] ] ) : | |||
leafletMap.getCenter(); | |||
leafletMap.setView( center, zoom, { animate: false } ); | |||
} ); | |||
} | |||
// Clickable region outlines (overview map). | |||
( custom.regions || [] ).forEach( function ( region ) { | |||
var latlngs = region.points.map( function ( p ) { | |||
return map.crs.fromPoint( [ p[ 1 ], p[ 0 ] ] ); // crs takes [y, x] | |||
} ), | |||
idle = { color: region.color || '#3b2a12', weight: 2, opacity: 0.55, fillColor: '#ffffff', fillOpacity: 0.03 }, | |||
hover = { opacity: 0.95, fillOpacity: 0.28 }, | |||
poly = new Leaflet.Polygon( latlngs, Object.assign( { className: 'dn-region', interactive: true }, idle ) ); | |||
poly.addTo( leafletMap ); | |||
poly.bindTooltip( region.name, { permanent: true, direction: 'center', interactive: false, opacity: 1, className: 'dn-region-label' } ); | |||
poly.on( 'mouseover', function () { poly.setStyle( hover ); } ); | |||
poly.on( 'mouseout', function () { poly.setStyle( idle ); } ); | |||
poly.on( 'click', function () { | |||
if ( region.article ) { | |||
window.location.href = mw.util.getUrl( region.article ); | |||
} | |||
} ); | |||
} ); | |||
} ); | } ); | ||
} ); | } ); | ||
} ); | } ); | ||
Latest revision as of 20:54, 29 August 2026
/**
* Interactive maps (Extension:DataMaps) site script.
* - Binds a permanent name label to every marker so the maps read like the printed ones,
* hiding labels of the busier groups while zoomed far out.
* - "Label-only" groups (LABEL_ONLY_GROUPS, or a map's `custom.labelOnlyGroups`): the icon is
* hidden and the name is drawn centred on the spot as an italic region name; clicking the
* name opens the marker's article. The marker itself still filters, searches and pops up.
* - Draws clickable, labelled region outlines from a map's `custom.regions`
* ([{ name, article, points: [[x,y], …] }]) — used by Map:Faerun to open the regional maps.
* - Applies a starting view (`custom.initialZoomIn` = number of zoom-button steps past the fitted
* view; `custom.initialCenter` = [x, y]) and unticks `custom.hiddenGroups` in the legend on load.
* Styling lives in MediaWiki:DataMaps.css.
*
* Label placement: by group (DIRECTION), overridable per marker by ending its id with
* --l, --r, --t or --b. A map can widen the set of "minor" (hidden-when-far) groups with
* `custom.labelMinorGroups`.
*/
mw.loader.using( [ 'ext.datamaps.bootstrap', 'ext.datamaps.leaflet', 'mediawiki.util' ] ).then( function ( require ) {
var Leaflet = require( 'ext.datamaps.leaflet' ),
MINOR_GROUPS = [ 'town', 'ruin', 'rumour' ],
LABEL_ONLY_GROUPS = [ 'wilds' ],
DIRECTION = { stronghold: 'bottom', skarhald: 'bottom', party: 'top' },
SUFFIX = { l: 'left', r: 'right', t: 'top', b: 'bottom' },
ZOOM_STEP = 0.25, // one click of the zoom buttons
// Minor labels appear once the map is zoomed in this many levels past its fitted (initial)
// zoom. 0.45 = two clicks.
NEAR_DELTA = 0.45;
function hideIcon( marker ) {
var el = marker.getElement && marker.getElement();
if ( el ) {
el.classList.add( 'dn-icon-hidden' );
}
}
mw.dataMaps.registerMapAddedHandler( function ( map ) {
var isMini = map.rootElement.classList.contains( 'ext-datamaps-mini-layout' ),
custom = map.config.custom || {},
minorGroups = custom.labelMinorGroups || MINOR_GROUPS,
labelOnlyGroups = custom.labelOnlyGroups || LABEL_ONLY_GROUPS;
map.on( 'markerReady', function ( marker ) {
var state = marker.apiInstance && marker.apiInstance[ 2 ],
group = marker.attachedLayers && marker.attachedLayers[ 0 ],
uid = state && state.uid ? String( state.uid ) : '',
suffix = /--([lrtb])$/.exec( uid ),
labelOnly = labelOnlyGroups.indexOf( group ) !== -1,
direction = labelOnly ? 'center' : ( ( suffix && SUFFIX[ suffix[ 1 ] ] ) || DIRECTION[ group ] || 'right' ),
size, half, offset, cls;
if ( !state || !state.label || typeof marker.bindTooltip !== 'function' ) {
return;
}
// Icons are anchored at their centre; push the label just clear of the icon edge.
size = marker.options && marker.options.icon && marker.options.icon.options.iconSize;
half = ( size ? ( Array.isArray( size ) ? size[ 0 ] : size.x || 40 ) : 24 ) / 2;
offset = direction === 'right' ? [ half + 2, 0 ] :
direction === 'left' ? [ -( half + 2 ), 0 ] :
direction === 'top' ? [ 0, -( half - 2 ) ] :
direction === 'bottom' ? [ 0, half - 2 ] : [ 0, 0 ];
cls = 'dn-map-label dn-map-label-' + direction + ' dn-map-label-g-' + ( group || 'none' );
if ( isMini ) {
cls += ' dn-map-label-mini';
}
if ( minorGroups.indexOf( group ) !== -1 ) {
cls += ' dn-map-label-minor';
}
if ( labelOnly ) {
cls += ' dn-map-label-only' + ( state.article ? ' dn-map-label-link' : '' );
hideIcon( marker );
marker.on( 'add', function () { hideIcon( marker ); } );
if ( state.article ) {
marker.on( 'tooltipopen', function ( e ) {
var el = e.tooltip && e.tooltip.getElement && e.tooltip.getElement();
if ( el && !el.dataset.dnLinked ) {
el.dataset.dnLinked = '1';
el.title = state.article;
el.addEventListener( 'click', function ( ev ) {
ev.stopPropagation();
window.location.href = mw.util.getUrl( state.article );
} );
}
} );
}
}
marker.bindTooltip( state.label, {
permanent: true,
direction: direction,
offset: offset,
interactive: labelOnly,
opacity: 1,
className: cls
} );
} );
// Groups that start unticked in the legend on this map (marker data still loads, so the
// reader can switch them on).
if ( custom.hiddenGroups && !isMini ) {
map.on( 'markerFilteringPanel', function () {
custom.hiddenGroups.forEach( function ( groupId ) {
var row = map.filtersPanel && map.filtersPanel.groupToggles[ groupId ];
if ( row ) {
row.checkbox.setSelected( false );
}
} );
} );
}
map.on( 'leafletLoaded', function () {
var leafletMap = map.viewport && map.viewport.getLeafletMap(),
baseZoom = null;
if ( !leafletMap ) {
return;
}
// Zoom tier → data attribute the CSS keys off. "far" = at or near the fitted zoom.
function updateTier() {
var z = leafletMap.getZoom();
if ( baseZoom === null || z < baseZoom ) {
baseZoom = z; // the most zoomed-out level seen is the fitted view
}
map.rootElement.dataset.dnZoom = z.toFixed( 2 );
map.rootElement.dataset.dnBaseZoom = baseZoom.toFixed( 2 );
map.rootElement.dataset.dnLabelTier = ( z - baseZoom ) < NEAR_DELTA ? 'far' : 'near';
}
leafletMap.on( 'zoomend', updateTier );
leafletMap.whenReady( updateTier );
// Starting view: zoom in a few steps from the fitted view, optionally around a point.
if ( !isMini && ( custom.initialZoomIn || custom.initialCenter ) ) {
leafletMap.whenReady( function () {
var zoom = leafletMap.getZoom() + ( custom.initialZoomIn || 0 ) * ZOOM_STEP,
center = custom.initialCenter ?
map.crs.fromPoint( [ custom.initialCenter[ 1 ], custom.initialCenter[ 0 ] ] ) :
leafletMap.getCenter();
leafletMap.setView( center, zoom, { animate: false } );
} );
}
// Clickable region outlines (overview map).
( custom.regions || [] ).forEach( function ( region ) {
var latlngs = region.points.map( function ( p ) {
return map.crs.fromPoint( [ p[ 1 ], p[ 0 ] ] ); // crs takes [y, x]
} ),
idle = { color: region.color || '#3b2a12', weight: 2, opacity: 0.55, fillColor: '#ffffff', fillOpacity: 0.03 },
hover = { opacity: 0.95, fillOpacity: 0.28 },
poly = new Leaflet.Polygon( latlngs, Object.assign( { className: 'dn-region', interactive: true }, idle ) );
poly.addTo( leafletMap );
poly.bindTooltip( region.name, { permanent: true, direction: 'center', interactive: false, opacity: 1, className: 'dn-region-label' } );
poly.on( 'mouseover', function () { poly.setStyle( hover ); } );
poly.on( 'mouseout', function () { poly.setStyle( idle ); } );
poly.on( 'click', function () {
if ( region.article ) {
window.location.href = mw.util.getUrl( region.article );
}
} );
} );
} );
} );
} );