Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions DefaultSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,43 @@
'NASAGIBS.ModisTerraChlorophyll' => true
],

// Custom named tile layers that can be used as base layers or overlays, in addition to the
// stock leaflet-providers layers listed above. The name of each definition becomes a valid
// value for the layers and overlays parameters. Only the definitions actually used by a map
// are sent to the browser.
//
// Each definition has a 'url' (required, an XYZ tile template or, for WMS, the service
// endpoint), an optional 'options' array passed straight to Leaflet, and an optional 'wms'
// flag. Definitions without a non-empty url string are ignored.
//
// A definition whose name matches a stock layer (e.g. 'OpenStreetMap') overrides that stock
// layer. The 'options' are passed to Leaflet as-is; note that 'attribution' is rendered as HTML.
//
// Example:
// 'egMapsLeafletLayerDefinitions' => [
// 'Historic 1904' => [
// 'url' => 'https://tiles.example.org/historic1904/{z}/{x}/{y}.png',
// 'options' => [
// 'attribution' => 'Historic map tiles',
// 'minZoom' => 1,
// 'maxZoom' => 18,
// 'subdomains' => 'abc',
// 'bounds' => [ [ 49.5, 5.8 ], [ 53.6, 15.2 ] ],
// ],
// ],
// 'Weather WMS' => [
// 'wms' => true,
// 'url' => 'https://example.org/geoserver/wms',
// 'options' => [
// 'layers' => 'weather:precipitation',
// 'format' => 'image/png',
// 'transparent' => true,
// 'attribution' => 'Weather service',
// ],
// ],
// ],
'egMapsLeafletLayerDefinitions' => [],

'egMapsLeafletLayersApiKeys' => [
'MapBox' => '',
'MapQuestOpen' => '',
Expand Down
6 changes: 6 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ different releases and which versions of PHP and MediaWiki they support, see the
[platform compatibility tables](INSTALL.md#platform-compatibility-and-release-status).


## Maps 13.1.0

Released on TBD.

* Added the `$egMapsLeafletLayerDefinitions` setting to define custom named Leaflet tile and WMS layers, which can then be used as values of the `layers` and `overlays` parameters

## Maps 13.0.2

Released on July 14th, 2026.
Expand Down
30 changes: 29 additions & 1 deletion resources/leaflet/jquery.leaflet.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@
};

this.newBaseLayerFromName = function(layerName) {
let definition = this.getLayerDefinition(layerName);
if (definition !== null) {
return this.newLayerFromDefinition(definition);
}

if (layerName === 'MapQuestOpen') {
return new window.MQ.TileLayer();
}
Expand All @@ -264,6 +269,24 @@
return new L.tileLayer.provider(layerName, layerOptions);
};

// Returns the custom layer definition for the given name, or null when there is none.
// Definitions are supplied per map via options.layerDefinitions (see LeafletService).
this.getLayerDefinition = function(layerName) {
if (options.layerDefinitions && Object.prototype.hasOwnProperty.call(options.layerDefinitions, layerName)) {
return options.layerDefinitions[layerName];
}

return null;
};

this.newLayerFromDefinition = function(definition) {
if (definition.wms) {
return L.tileLayer.wms(definition.url, definition.options);
}

return L.tileLayer(definition.url, definition.options);
};

this.getImageBaseLayers = function() {
let layers = new Map();

Expand Down Expand Up @@ -291,7 +314,12 @@
let overlays = {};

$.each(options.overlays, function(index, overlayName) {
overlays[mw.html.escape(overlayName)] = new L.tileLayer.provider(overlayName).addTo(_this.map);
let definition = _this.getLayerDefinition(overlayName);
let overlay = definition !== null
? _this.newLayerFromDefinition(definition)
: new L.tileLayer.provider(overlayName);

overlays[mw.html.escape(overlayName)] = overlay.addTo(_this.map);
});

return overlays;
Expand Down
83 changes: 83 additions & 0 deletions src/LeafletLayerDefinitions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare( strict_types = 1 );

namespace Maps;

/**
* Admin-defined custom Leaflet base and overlay layers, configured via
* $egMapsLeafletLayerDefinitions. Normalizes and validates the raw configuration and is the
* single source of truth for which custom layer names exist and what their definition is.
*
* @licence GNU GPL v2+
*/
class LeafletLayerDefinitions {

/**
* @var array<string, array{url: string, options: array, wms: bool}>
*/
private array $definitions;

/**
* @param array $rawDefinitions Layer name to raw definition, as configured via
* $egMapsLeafletLayerDefinitions. Invalid definitions are skipped.
*/
public function __construct( array $rawDefinitions ) {
$this->definitions = $this->normalizeAll( $rawDefinitions );
}

private function normalizeAll( array $rawDefinitions ): array {
$definitions = [];

foreach ( $rawDefinitions as $name => $rawDefinition ) {
$definition = $this->normalize( $rawDefinition );

if ( $definition !== null ) {
$definitions[$name] = $definition;
}
}

return $definitions;
}

/**
* @return array{url: string, options: array, wms: bool}|null
*/
private function normalize( mixed $rawDefinition ): ?array {
if ( !is_array( $rawDefinition ) ) {
return null;
}

$url = $rawDefinition['url'] ?? null;

if ( !is_string( $url ) || $url === '' ) {
return null;
}

return [
'url' => $url,
'options' => is_array( $rawDefinition['options'] ?? null ) ? $rawDefinition['options'] : [],
'wms' => (bool)( $rawDefinition['wms'] ?? false ),
];
}

/**
* @return string[]
*/
public function getLayerNames(): array {
return array_keys( $this->definitions );
}

/**
* @param string[] $names
* @return array<string, array{url: string, options: array, wms: bool}> Normalized definitions
* for the requested names that are defined. Unknown names are omitted.
*/
public function getDefinitions( array $names ): array {
return array_intersect_key(
$this->definitions,
array_fill_keys( $names, true )
);
}

}
59 changes: 54 additions & 5 deletions src/LeafletService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
class LeafletService implements MappingService {

private ImageRepository $imageFinder;
private LeafletLayerDefinitions $layerDefinitions;
private array $addedDependencies = [];

public function __construct( ImageRepository $imageFinder ) {
public function __construct( ImageRepository $imageFinder, LeafletLayerDefinitions $layerDefinitions ) {
$this->imageFinder = $imageFinder;
$this->layerDefinitions = $layerDefinitions;
}

public function getName(): string {
Expand Down Expand Up @@ -51,7 +53,7 @@ public function getParameterInfo(): array {
'aliases' => 'layer',
'type' => 'string',
'islist' => true,
'values' => array_keys( $GLOBALS['egMapsLeafletAvailableLayers'], true, true ),
'values' => $this->availableLayerNames( $GLOBALS['egMapsLeafletAvailableLayers'] ),
'default' => $GLOBALS['egMapsLeafletLayers'],
'message' => 'maps-leaflet-par-layers',
];
Expand All @@ -68,7 +70,7 @@ public function getParameterInfo(): array {
'aliases' => [ 'overlaylayers' ],
'type' => ParameterTypes::STRING,
'islist' => true,
'values' => array_keys( $GLOBALS['egMapsLeafletAvailableOverlayLayers'], true, true ),
'values' => $this->availableLayerNames( $GLOBALS['egMapsLeafletAvailableOverlayLayers'] ),
'default' => $GLOBALS['egMapsLeafletOverlayLayers'],
'message' => 'maps-leaflet-par-overlaylayers',
];
Expand Down Expand Up @@ -145,6 +147,24 @@ public function getParameterInfo(): array {
return $params;
}

/**
* The enabled stock layer names plus the names of the custom layer definitions, which are
* valid values for both the layers and overlays parameters.
*
* @param array<string, bool> $availableStockLayers
* @return string[]
*/
private function availableLayerNames( array $availableStockLayers ): array {
return array_values(
array_unique(
array_merge(
array_keys( $availableStockLayers, true, true ),
$this->layerDefinitions->getLayerNames()
)
)
);
}

/**
* @since 3.0
*/
Expand Down Expand Up @@ -243,13 +263,15 @@ public function newMapDataFromParameters( array $params ): MapData {

$params['overlays'] = $this->filterToAvailable(
$params['overlays'],
$GLOBALS['egMapsLeafletAvailableOverlayLayers']
$this->availableWithDefinitions( $GLOBALS['egMapsLeafletAvailableOverlayLayers'] )
);
$params['layers'] = $this->filterToAvailable(
$params['layers'],
$GLOBALS['egMapsLeafletAvailableLayers']
$this->availableWithDefinitions( $GLOBALS['egMapsLeafletAvailableLayers'] )
);

$params = $this->addUsedLayerDefinitions( $params );

return new MapData( $params );
}

Expand All @@ -271,6 +293,33 @@ private function filterToAvailable( array $values, array $available ): array {
);
}

/**
* @param array<string, bool> $available
* @return array<string, bool>
*/
private function availableWithDefinitions( array $available ): array {
// Union rather than array_merge: array_merge renumbers integer-like keys, which would drop
// a custom layer whose name is purely numeric (e.g. "1904"). Custom names go on the left so
// they take precedence over a same-named stock layer.
return array_fill_keys( $this->layerDefinitions->getLayerNames(), true ) + $available;
}

/**
* Serializes the custom layer definitions actually used by this map into the map data, so only
* the relevant definitions are shipped to the client rather than the whole catalog.
*/
private function addUsedLayerDefinitions( array $params ): array {
$usedDefinitions = $this->layerDefinitions->getDefinitions(
array_merge( $params['layers'], $params['overlays'] )
);

if ( $usedDefinitions !== [] ) {
$params['layerDefinitions'] = $usedDefinitions;
}

return $params;
}

private function getJsImageLayers( array $imageLayers ) {
$jsImageLayers = [];

Expand Down
7 changes: 6 additions & 1 deletion src/MapsFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,17 @@ private function getGoogleMapsService(): GoogleMapsService {

private function getLeafletService(): LeafletService {
$this->leafletService ??= new LeafletService(
$this->getImageRepository()
$this->getImageRepository(),
$this->getLeafletLayerDefinitions()
);

return $this->leafletService;
}

public function getLeafletLayerDefinitions(): LeafletLayerDefinitions {
return new LeafletLayerDefinitions( $this->settings['egMapsLeafletLayerDefinitions'] ?? [] );
}

public function getDisplayMapFunction(): DisplayMapFunction {
return new DisplayMapFunction(
$this->getMappingServices()
Expand Down
Loading
Loading