/**
* @module modules/maps.js
* @name maps
* @copyright 2023 3Liz
* @license MPL-2.0
*/
import { mainLizmap, mainEventDispatcher } from './Globals.js';
import Utils from './Utils.js';
import { BaseLayerTypes } from './config/BaseLayer.js';
import { MapLayerLoadStatus, MapLayerState } from './state/MapLayer.js';
import olMap from 'ol/Map.js';
import View from 'ol/View.js';
import { ADJUSTED_DPI } from '../utils/Constants.js';
import { get as getProjection, getPointResolution } from 'ol/proj.js';
import { Attribution } from 'ol/control.js';
import ImageWMS from 'ol/source/ImageWMS.js';
import WMTS, {optionsFromCapabilities} from 'ol/source/WMTS.js';
import { WMTSCapabilities, GeoJSON, WKT } from 'ol/format.js';
import WMTSTileGrid from 'ol/tilegrid/WMTS.js';
import {getWidth} from 'ol/extent.js';
import { Image as ImageLayer, Tile as TileLayer } from 'ol/layer.js';
import TileGrid from 'ol/tilegrid/TileGrid.js';
import TileWMS from 'ol/source/TileWMS.js';
import XYZ from 'ol/source/XYZ.js';
import BingMaps from 'ol/source/BingMaps.js';
import Google from 'ol/source/Google.js';
import { BaseLayer as LayerBase } from 'ol/layer/Base.js';
import LayerGroup from 'ol/layer/Group.js';
import { Vector as VectorSource } from 'ol/source.js';
import { Vector as VectorLayer } from 'ol/layer.js';
import DragZoom from 'ol/interaction/DragZoom.js';
import { always } from 'ol/events/condition.js';
import SingleWMSLayer from './SingleWMSLayer.js';
/**
* Class initializing Openlayers Map.
* @class
* @name map
* @augments olMap
*/
export default class map extends olMap {
constructor() {
const qgisProjectProjection = mainLizmap.projection;
const mapProjection = getProjection(qgisProjectProjection);
// Get resolutions from OL2 map
let resolutions = mainLizmap.lizmap3.map.resolutions ? mainLizmap.lizmap3.map.resolutions : mainLizmap.lizmap3.map.baseLayer.resolutions;
if (resolutions == undefined) {
resolutions= [mainLizmap.lizmap3.map.resolution];
}
// Remove duplicated values
resolutions = [... new Set(resolutions)];
// Sorting in descending order
resolutions = resolutions.sort((a, b) => a < b);
super({
controls: [
new Attribution({ target: 'attribution-ol', collapsed: false })
],
view: new View({
resolutions: resolutions,
constrainResolution: true,
center: [mainLizmap.lizmap3.map.getCenter().lon, mainLizmap.lizmap3.map.getCenter().lat],
projection: mapProjection,
extent: mainLizmap.lizmap3.map.restrictedExtent.toArray(),
constrainOnlyCenter: true // allow view outside the restricted extent when zooming
}),
target: 'newOlMap'
});
this._newOlMap = true;
// Zoom to box
this._dragZoom = new DragZoom({
condition: always
});
this._dragZoom.setActive(false);
this.addInteraction(this._dragZoom);
this._dispatchMapStateChanged = () => {
const view = this.getView();
const projection = view.getProjection();
const dpi = ADJUSTED_DPI;
const inchesPerMeter = 1000 / 25.4;
const resolution = view.getResolution();
const scaleDenominator = resolution * inchesPerMeter * dpi;
// The Scale line control uses this method to defined scale denominator
const pointResolution = getPointResolution(projection, view.getResolution(), view.getCenter(), projection.getUnits());
const pointScaleDenominator = pointResolution * inchesPerMeter * dpi;
mainLizmap.state.map.update({
'type': 'map.state.changing',
'projection': projection.getCode(),
'center': [...view.getCenter()],
'zoom': view.getZoom(),
'size': [...this.getSize()],
'extent': view.calculateExtent(),
'resolution': resolution,
'scaleDenominator': scaleDenominator,
'pointResolution': pointResolution,
'pointScaleDenominator': pointScaleDenominator,
});
};
// Disable High DPI for requests
this._hidpi = false;
// Ratio between WMS single tiles and map viewport
this._WMSRatio = 1.1;
// Respecting WMS max size
const wmsMaxSize = [
mainLizmap.initialConfig.options.wmsMaxWidth,
mainLizmap.initialConfig.options.wmsMaxHeight,
];
// Get pixel ratio, if High DPI is disabled do not use device pixel ratio
const pixelRatio = this._hidpi ? this.pixelRatio_ : 1;
this._useCustomTileWms = this.getSize().reduce(
(r /*accumulator*/, x /*currentValue*/, i /*currentIndex*/) => r || Math.ceil(x*this._WMSRatio*pixelRatio) > wmsMaxSize[i],
false,
);
this._customTileGrid = this._useCustomTileWms ? new TileGrid({
extent: mainLizmap.lizmap3.map.restrictedExtent.toArray(),
resolutions: resolutions,
tileSize: this.getSize().map((x, i) => {
// Get the min value between the map size and the max size
// divided by pixel ratio
const vMin = Math.min(
Math.floor(x/pixelRatio),
Math.floor(wmsMaxSize[i]/pixelRatio)
);
// If the min value with a margin of WMS ratio is less
// than max size divided by pixel ratio the keep it
if (vMin*this._WMSRatio < wmsMaxSize[i]/pixelRatio) {
return vMin;
}
// Else get the min value divided by WMS ratio
return Math.floor(vMin/this._WMSRatio);
})
}) : null;
// Mapping between states and OL layers and groups
this._statesOlLayersandGroupsMap = new Map();
// Array of layers and groups in overlayLayerGroup
this._overlayLayersAndGroups = [];
// Mapping between layers name and states used to construct the singleWMSLayer, if needed
this._statesSingleWMSLayers = new Map();
const layersCount = mainLizmap.state.rootMapGroup.countExplodedMapLayers();
// Returns a layer or a layerGroup depending of the node type
const createNode = (node, statesOlLayersandGroupsMap, overlayLayersAndGroups, metersPerUnit, WMSRatio) => {
if(node.type === 'group'){
const layers = [];
for (const layer of node.children.slice().reverse()) {
// Keep only layers with a geometry and groups
if(node.type !== 'layer' && node.type !== 'group'){
continue;
}
let newNode = createNode(layer, statesOlLayersandGroupsMap, overlayLayersAndGroups, metersPerUnit, WMSRatio)
if(newNode){
layers.push(newNode);
}
}
const layerGroup = new LayerGroup({
layers: layers
});
if (node.name !== 'root') {
layerGroup.setVisible(node.visibility);
layerGroup.setProperties({
name: node.name
});
statesOlLayersandGroupsMap.set(node.name, [node, layerGroup]);
overlayLayersAndGroups.push(layerGroup);
}
return layerGroup;
} else {
let layer;
// Keep only layers with a geometry
if(node.type !== 'layer'){
return;
}
/* Sometimes throw an Error and extent is not used
let extent = node.layerConfig.extent;
if(node.layerConfig.crs !== "" && node.layerConfig.crs !== mainLizmap.projection){
extent = transformExtent(extent, node.layerConfig.crs, mainLizmap.projection);
}
*/
// Set min/max resolution only if different from default
let minResolution = node.wmsMinScaleDenominator <= 1 ? undefined : Utils.getResolutionFromScale(node.layerConfig.minScale, metersPerUnit);
let maxResolution = node.wmsMaxScaleDenominator <= 1 ? undefined : Utils.getResolutionFromScale(node.layerConfig.maxScale, metersPerUnit);
// The layer is configured to be cached
if (node.layerConfig.cached) {
// Using WMTS
const parser = new WMTSCapabilities();
const result = parser.read(lizMap.wmtsCapabilities);
// Build WMTS options
let options;
if (result['Contents']['Layer']) {
options = optionsFromCapabilities(result, {
layer: node.wmsName,
matrixSet: mainLizmap.projection,
});
}
// The options could be null if the layer has not be found in
// WMTS capabilities
if (options) {
layer = new TileLayer({
minResolution: minResolution,
maxResolution: maxResolution,
source: new WMTS(options)
});
}
} else {
if(mainLizmap.state.map.singleWMSLayer){
this._statesSingleWMSLayers.set(node.name,node);
node.singleWMSLayer = true;
return
} else {
const itemState = node.itemState;
const useExternalAccess = (itemState.externalWmsToggle && itemState.externalAccess.type !== 'wmts' && itemState.externalAccess.type !== 'xyz')
if (this._useCustomTileWms) {
layer = new TileLayer({
minResolution: minResolution,
maxResolution: maxResolution,
source: new TileWMS({
url: useExternalAccess ? itemState.externalAccess.url : mainLizmap.serviceURL,
serverType: 'qgis',
tileGrid: this._customTileGrid,
params: {
LAYERS: useExternalAccess ? decodeURIComponent(itemState.externalAccess.layers) : node.wmsName,
FORMAT: useExternalAccess ? decodeURIComponent(itemState.externalAccess.format) : node.layerConfig.imageFormat,
STYLES: useExternalAccess ? decodeURIComponent(itemState.externalAccess.styles) : node.wmsSelectedStyleName,
DPI: 96,
TILED: 'true'
},
wrapX: false, // do not reused across the 180° meridian.
hidpi: this._hidpi, // pixelRatio is used in useTileWms and customTileGrid definition
})
});
// Force no cache w/ Firefox
if(navigator.userAgent.includes("Firefox")){
layer.getSource().setTileLoadFunction((image, src) => {
(image.getImage()).src = src + '&ts=' + Date.now();
});
}
} else if (!node.layerConfig.singleTile) {
layer = new TileLayer({
minResolution: minResolution,
maxResolution: maxResolution,
source: new TileWMS({
url: useExternalAccess ? itemState.externalAccess.url : mainLizmap.serviceURL,
serverType: 'qgis',
params: {
LAYERS: useExternalAccess ? decodeURIComponent(itemState.externalAccess.layers) : node.wmsName,
FORMAT: useExternalAccess ? decodeURIComponent(itemState.externalAccess.format) : node.layerConfig.imageFormat,
STYLES: useExternalAccess ? decodeURIComponent(itemState.externalAccess.styles) : node.wmsSelectedStyleName,
DPI: 96,
TILED: 'true'
},
}),
});
} else {
layer = new ImageLayer({
// extent: extent,
minResolution: minResolution,
maxResolution: maxResolution,
source: new ImageWMS({
url: useExternalAccess ? itemState.externalAccess.url : mainLizmap.serviceURL,
serverType: 'qgis',
ratio: WMSRatio,
hidpi: this._hidpi,
params: {
LAYERS: useExternalAccess ? decodeURIComponent(itemState.externalAccess.layers) : node.wmsName,
FORMAT: useExternalAccess ? decodeURIComponent(itemState.externalAccess.format) : node.layerConfig.imageFormat,
STYLES: useExternalAccess ? decodeURIComponent(itemState.externalAccess.styles) : node.wmsSelectedStyleName,
DPI: 96
},
})
});
// Force no cache w/ Firefox
if(navigator.userAgent.includes("Firefox")){
layer.getSource().setImageLoadFunction((image, src) => {
(image.getImage()).src = src + '&ts=' + Date.now();
});
}
}
}
}
if(layer){
layer.setVisible(node.visibility);
layer.setOpacity(node.opacity);
layer.setProperties({
name: node.name
});
layer.getSource().setProperties({
name: node.name
});
// OL layers zIndex is the reverse of layer's order given by cfg
layer.setZIndex(layersCount - 1 - node.layerOrder);
// Add attribution
if (node.wmsAttribution != null) {
const url = node.wmsAttribution.url;
const title = node.wmsAttribution.title;
let attribution = title;
if (url) {
attribution = `<a href='${url}' target='_blank'>${title}</a>`;
}
layer.getSource().setAttributions(attribution);
}
overlayLayersAndGroups.push(layer);
statesOlLayersandGroupsMap.set(node.name, [node, layer]);
return layer;
}
}
}
this._overlayLayersGroup = new LayerGroup();
const metersPerUnit = this.getView().getProjection().getMetersPerUnit();
if(mainLizmap.state.layerTree.children.length){
this._overlayLayersGroup = createNode(
mainLizmap.state.rootMapGroup,
this._statesOlLayersandGroupsMap,
this._overlayLayersAndGroups,
metersPerUnit,
this._WMSRatio
);
}
this._overlayLayersGroup.setProperties({
name: 'LizmapOverLayLayersGroup'
});
// Get the max layers zIndex
const maxZIndex = this.overlayLayers.map((layer) => layer.getZIndex()).reduce(
(maxValue, currentValue) => maxValue <= currentValue ? currentValue : maxValue,
0
);
// Get the base layers zIndex which is the layer min zIndex - 1
// to be sure base layers are under the others layers
const baseLayerZIndex = this.overlayLayers.map((layer) => layer.getZIndex()).reduce(
(minValue, currentValue) => minValue <= currentValue ? minValue : currentValue,
0
) - 1;
const proj3857 = getProjection('EPSG:3857');
const max3857Resolution = getWidth(proj3857.getExtent()) / 256;
const map3857Resolutions = [max3857Resolution];
while (map3857Resolutions.at(-1) > resolutions.at(-1)) {
map3857Resolutions.push(max3857Resolution / Math.pow(2, map3857Resolutions.length));
}
this._hasEmptyBaseLayer = false;
const baseLayers = [];
for (const baseLayerState of mainLizmap.state.baseLayers.getBaseLayers()) {
let baseLayer;
let layerMinResolution;
let layerMaxResolution;
if (baseLayerState.hasItemState && baseLayerState.hasLayerConfig) {
layerMinResolution = baseLayerState.itemState.wmsMinScaleDenominator <= 1 ? undefined : Utils.getResolutionFromScale(baseLayerState.layerConfig.minScale, metersPerUnit);
layerMaxResolution = baseLayerState.itemState.wmsMaxScaleDenominator <= 1 ? undefined : Utils.getResolutionFromScale(baseLayerState.layerConfig.maxScale, metersPerUnit);
}
if (baseLayerState.type === BaseLayerTypes.XYZ) {
const tileGrid =new TileGrid({
origin: [-20037508, 20037508],
resolutions: map3857Resolutions,
});
tileGrid.minZoom = baseLayerState.zmin;
tileGrid.maxZoom = baseLayerState.zmax;
baseLayer = new TileLayer({
minResolution: layerMinResolution,
maxResolution: layerMaxResolution,
source: new XYZ({
url: baseLayerState.url,
projection: baseLayerState.crs,
minZoom: baseLayerState.zmin,
maxZoom: baseLayerState.zmax,
tileGrid : tileGrid,
})
});
} else if (baseLayerState.type === BaseLayerTypes.WMS) {
baseLayer = new ImageLayer({
minResolution: layerMinResolution,
maxResolution: layerMaxResolution,
source: new ImageWMS({
url: baseLayerState.url,
projection: baseLayerState.crs,
ratio: this._WMSRatio,
params: {
LAYERS: baseLayerState.layers,
STYLES: baseLayerState.styles,
FORMAT: baseLayerState.format
},
})
});
} else if (baseLayerState.type === BaseLayerTypes.WMTS) {
const tileGrid = new WMTSTileGrid({
origin: [-20037508, 20037508],
resolutions: map3857Resolutions,
matrixIds: map3857Resolutions.map((r, i) => i.toString()),
});
tileGrid.maxZoom = baseLayerState.numZoomLevels;
let url = baseLayerState.url;
if(baseLayerState.key && url.includes('{key}')){
url = url.replaceAll('{key}', baseLayerState.key);
}
baseLayer = new TileLayer({
minResolution: layerMinResolution,
maxResolution: layerMaxResolution,
source: new WMTS({
url: url,
layer: baseLayerState.layer,
matrixSet: baseLayerState.matrixSet,
format: baseLayerState.format,
projection: baseLayerState.crs,
tileGrid: tileGrid,
style: baseLayerState.style
})
});
} else if (baseLayerState.type === BaseLayerTypes.Bing) {
baseLayer = new TileLayer({
minResolution: layerMinResolution,
maxResolution: layerMaxResolution,
preload: Infinity,
source: new BingMaps({
key: baseLayerState.key,
imagerySet: baseLayerState.imagerySet,
// use maxZoom 19 to see stretched tiles instead of the BingMaps
// "no photos at this zoom level" tiles
// maxZoom: 19
}),
});
} else if (baseLayerState.type === BaseLayerTypes.Google) {
baseLayer = new TileLayer({
minResolution: layerMinResolution,
maxResolution: layerMaxResolution,
preload: Infinity,
source: new Google({
key: baseLayerState.key,
mapType: baseLayerState.mapType,
}),
});
} else if (baseLayerState.type === BaseLayerTypes.Lizmap) {
if (baseLayerState.layerConfig.cached) {
const parser = new WMTSCapabilities();
const result = parser.read(lizMap.wmtsCapabilities);
const options = optionsFromCapabilities(result, {
layer: baseLayerState.itemState.wmsName,
matrixSet: mainLizmap.projection,
});
baseLayer = new TileLayer({
minResolution: layerMinResolution,
maxResolution: layerMaxResolution,
source: new WMTS(options)
});
} else {
if(mainLizmap.state.map.singleWMSLayer){
baseLayerState.singleWMSLayer = true;
this._statesSingleWMSLayers.set(baseLayerState.name, baseLayerState);
} else {
if (this._useCustomTileWms) {
baseLayer = new TileLayer({
// extent: extent,
minResolution: layerMinResolution,
maxResolution: layerMaxResolution,
source: new TileWMS({
url: mainLizmap.serviceURL,
projection: qgisProjectProjection,
serverType: 'qgis',
tileGrid: this._customTileGrid,
params: {
LAYERS: baseLayerState.itemState.wmsName,
FORMAT: baseLayerState.layerConfig.imageFormat,
DPI: 96,
TILED: 'true'
},
wrapX: false, // do not reused across the 180° meridian.
hidpi: this._hidpi, // pixelRatio is used in useTileWms and customTileGrid definition
})
});
} else {
baseLayer = new ImageLayer({
// extent: extent,
minResolution: layerMinResolution,
maxResolution: layerMaxResolution,
source: new ImageWMS({
url: mainLizmap.serviceURL,
projection: qgisProjectProjection,
serverType: 'qgis',
ratio: this._WMSRatio,
hidpi: this._hidpi,
params: {
LAYERS: baseLayerState.itemState.wmsName,
FORMAT: baseLayerState.layerConfig.imageFormat,
DPI: 96
},
})
});
}
}
}
} else if (baseLayerState.type === BaseLayerTypes.Empty) {
this._hasEmptyBaseLayer = true;
}
if (!baseLayer) {
continue;
}
if (baseLayerState.hasAttribution) {
const url = baseLayerState.attribution.url;
const title = baseLayerState.attribution.title;
let attribution = title;
if (url) {
attribution = `<a href='${url}' target='_blank'>${title}</a>`;
}
baseLayer.getSource().setAttributions(attribution);
}
const visible = mainLizmap.initialConfig.baseLayers.startupBaselayerName === baseLayerState.name;
baseLayer.setProperties({
name: baseLayerState.name,
title: baseLayerState.title,
visible: visible
});
// Force baselayer to be under the others layers
baseLayer.setZIndex(baseLayerZIndex);
baseLayers.push(baseLayer);
if (visible && baseLayer.getSource().getProjection().getCode() !== qgisProjectProjection) {
this.getView().getProjection().setExtent(mainLizmap.lizmap3.map.restrictedExtent.toArray());
}
}
this._baseLayersGroup;
if (baseLayers.length) {
this._baseLayersGroup = new LayerGroup({
layers: baseLayers
});
} else {
this._baseLayersGroup = new LayerGroup();
}
this._baseLayersGroup.setProperties({
name: 'LizmapBaseLayersGroup'
});
this._singleImageWmsGroup = new LayerGroup();
this._singleImageWmsGroup.setProperties({
name: 'LizmapSingleImageWmsGroup'
});
if (this._statesSingleWMSLayers.size > 0) {
//create new Image layer and add it to the map
const singleWMSLayer = new SingleWMSLayer(this);
// create a new group
this._singleImageWmsGroup = new LayerGroup({
layers:[singleWMSLayer.layer]
});
}
this._toolsGroup = new LayerGroup();
this._toolsGroup.setZIndex(maxZIndex+2);
this._toolsGroup.setProperties({
name: 'LizmapToolsGroup'
});
// Add base and overlay layers to the map's main LayerGroup
this.setLayerGroup(new LayerGroup({
layers: [this._baseLayersGroup, this._singleImageWmsGroup, this._overlayLayersGroup, this._toolsGroup]
}));
// Sync new OL view with OL2 view
mainLizmap.lizmap3.map.events.on({
move: () => {
this.syncNewOLwithOL2View();
}
});
this.on('moveend', () => {
this._dispatchMapStateChanged();
if (!mainLizmap.newOlMap) {
lizMap.map.setCenter(undefined,this.getView().getZoom(), false, false);
}
});
// Init view
this.syncNewOLwithOL2View();
// Listen/Dispatch events
this.getView().on('change', () => {
if (this.isDragZoomActive) {
this.deactivateDragZoom();
}
});
this.getView().on('change:resolution', () => {
mainEventDispatcher.dispatch('resolution.changed');
});
this._baseLayersGroup.on('change', () => {
mainEventDispatcher.dispatch('baseLayers.changed');
});
this._overlayLayersGroup.on('change', () => {
mainEventDispatcher.dispatch('overlayLayers.changed');
});
for (const layer of this.overlayLayers) {
const source = layer.getSource();
if (source instanceof ImageWMS) {
source.on('imageloadstart', event => {
const mapLayer = mainLizmap.state.rootMapGroup.getMapLayerByName(event.target.get('name'))
mapLayer.loadStatus = MapLayerLoadStatus.Loading;
});
source.on('imageloadend', event => {
const mapLayer = mainLizmap.state.rootMapGroup.getMapLayerByName(event.target.get('name'))
mapLayer.loadStatus = MapLayerLoadStatus.Ready;
});
source.on('imageloaderror', event => {
const mapLayer = mainLizmap.state.rootMapGroup.getMapLayerByName(event.target.get('name'))
mapLayer.loadStatus = MapLayerLoadStatus.Error;
});
} else if (source instanceof WMTS) {
source.on('tileloadstart', event => {
const mapLayer = mainLizmap.state.rootMapGroup.getMapLayerByName(event.target.get('name'))
mapLayer.loadStatus = MapLayerLoadStatus.Loading;
});
source.on('tileloadend', event => {
const mapLayer = mainLizmap.state.rootMapGroup.getMapLayerByName(event.target.get('name'))
mapLayer.loadStatus = MapLayerLoadStatus.Ready;
});
source.on('tileloaderror', event => {
const mapLayer = mainLizmap.state.rootMapGroup.getMapLayerByName(event.target.get('name'))
mapLayer.loadStatus = MapLayerLoadStatus.Error;
});
}
}
mainLizmap.state.rootMapGroup.addListener(
evt => {
// if the layer is loaded ad single WMS, the visibility events are managed by the dedicated class
if (this.isSingleWMSLayer(evt.name)) return;
const olLayerOrGroup = this.getLayerOrGroupByName(evt.name);
if (olLayerOrGroup) {
olLayerOrGroup.setVisible(evt.visibility);
} else {
console.log('`'+evt.name+'` is not an OpenLayers layer or group!');
}
},
['layer.visibility.changed', 'group.visibility.changed']
);
mainLizmap.state.layersAndGroupsCollection.addListener(
evt => {
// conservative control since the opacity events should not be fired for single WMS layers
if (this.isSingleWMSLayer(evt.name)) return;
const activeBaseLayer = this.getActiveBaseLayer();
if (activeBaseLayer && activeBaseLayer.get("name") === evt.name) {
activeBaseLayer.setOpacity(evt.opacity);
} else {
this.getLayerOrGroupByName(evt.name)?.setOpacity(evt.opacity);
}
},
['layer.opacity.changed', 'group.opacity.changed']
);
mainLizmap.state.rootMapGroup.addListener(
evt => {
const stateOlLayerAndMap = this._statesOlLayersandGroupsMap.get(evt.name);
if (!stateOlLayerAndMap) return;
const [state, olLayer] = stateOlLayerAndMap;
const wmsParams = olLayer.getSource().getParams();
// Delete entries in `wmsParams` not in `state.wmsParameters`
for(const key of Object.keys(wmsParams)){
if(!Object.hasOwn(state.wmsParameters, key)){
delete wmsParams[key];
}
}
Object.assign(wmsParams, state.wmsParameters);
olLayer.getSource().updateParams(wmsParams);
},
['layer.symbol.checked.changed', 'layer.style.changed', 'layer.selection.token.changed', 'layer.filter.token.changed']
);
mainLizmap.state.baseLayers.addListener(
evt => {
this.changeBaseLayer(evt.name);
},
['baselayers.selection.changed']
);
mainLizmap.state.rootMapGroup.addListener(
evt => {
const extGroup = mainLizmap.state.rootMapGroup.children[0];
if (evt.name != extGroup.name)
return;
const extLayerGroup = new LayerGroup({
layers: []
});
extLayerGroup.setVisible(extGroup.visibility);
extLayerGroup.setZIndex(maxZIndex+1);
extLayerGroup.setProperties({
name: extGroup.name,
type: 'ext-group'
});
this._overlayLayersGroup.getLayers().push(extLayerGroup);
extGroup.addListener(
evtLayer => {
const extLayer = extGroup.children[0];
if (evtLayer.childName != extLayer.name)
return;
extLayer.olLayer.setProperties({
name: evtLayer.childName,
type: 'ol-layer'
});
extLayerGroup.getLayers().push(extLayer.olLayer);
}, ['ol-layer.added']
);
extGroup.addListener(
evtLayer => {
const layers = extLayerGroup
.getLayers()
.getArray()
.filter((item) => item.get('name') == evtLayer.childName);
if (layers.length == 0)
return;
extLayerGroup.getLayers().remove(layers[0]);
}, ['ol-layer.removed']
);
}, ['ext-group.added']
);
mainLizmap.state.rootMapGroup.addListener(
evt => {
const groups = this._overlayLayersGroup
.getLayers()
.getArray()
.filter((item) => item.get('name') == evt.childName && item.get('type') == 'ext-group');
if (groups.length == 0)
return;
this._overlayLayersGroup.getLayers().remove(groups[0]);
}, ['ext-group.removed']
);
// Create the highlight layer
// used to display features on top of all layers
const styleColor = 'rgba(255,255,0,0.8)';
const styleWidth = 3;
this._highlightLayer = new VectorLayer({
source: new VectorSource({
wrapX: false
}),
style: {
'circle-stroke-color': styleColor,
'circle-stroke-width': styleWidth,
'circle-radius': 6,
'stroke-color': styleColor,
'stroke-width': styleWidth,
}
});
this.addToolLayer(this._highlightLayer);
// Add startup features to map if any
const startupFeatures = mainLizmap.state.map.startupFeatures;
if (startupFeatures) {
this.setHighlightFeatures(startupFeatures, "geojson");
}
mainLizmap.state.map.addListener(
evt => {
const view = this.getView();
const updateCenter = ('center' in evt && view.getCenter().filter((v, i) => {return evt['center'][i] != v}).length != 0);
const updateZoom = ('zoom' in evt && evt['zoom'] !== view.getZoom());
const updateResolution = ('resolution' in evt && evt['resolution'] !== view.getResolution());
const updateExtent = ('extent' in evt && view.calculateExtent().filter((v, i) => {return evt['extent'][i] != v}).length != 0);
if (updateCenter && updateResolution) {
view.animate({
center: evt['center'],
resolution: evt['resolution'],
duration: 50
});
} else if (updateCenter) {
view.setCenter(evt['center']);
} else if (updateZoom) {
view.animate({
zoom: evt['zoom'],
duration: 250
});
} else if (updateResolution) {
view.setResolution(evt['resolution']);
} else if (updateExtent) {
view.fit(evt['extent'], {nearest: true});
}
},
['map.state.changed']
);
}
get hasEmptyBaseLayer() {
return this._hasEmptyBaseLayer;
}
get baseLayersGroup(){
return this._baseLayersGroup;
}
get toolsGroup(){
return this._toolsGroup;
}
get overlayLayersAndGroups(){
return this._overlayLayersAndGroups;
}
// Get overlay layers (not layerGroups)
get overlayLayers(){
return this._overlayLayersGroup.getLayersArray();
}
get overlayLayersGroup(){
return this._overlayLayersGroup;
}
/**
* Key (name) / value (state) Map of layers loaded in a single WMS image
* @type {Map}
*/
get statesSingleWMSLayers(){
return this._statesSingleWMSLayers;
}
/**
* Map and base Layers are loaded as TileWMS
* @type {boolean}
*/
get useTileWms(){
return this._useCustomTileWms;
}
/**
* TileGrid configuration when layers is loaded as TileWMS
* @type {null|TileGrid}
*/
get customTileGrid(){
return this._customTileGrid;
}
/**
* WMS/TileWMS high dpi support
* @type {boolean}
*/
get hidpi(){
return this._hidpi;
}
/**
* Is dragZoom active?
* @type {boolean}
*/
get isDragZoomActive(){
return this._dragZoom.getActive();
}
/**
* Add highlight features on top of all layer
* @param {string} features features as GeoJSON or WKT
* @param {string} format format string as `geojson` or `wkt`
* @param {string|undefined} projection optional features projection
*/
addHighlightFeatures(features, format, projection) {
let olFeatures;
if (format === "geojson") {
olFeatures = (new GeoJSON()).readFeatures(features, {
dataProjection: projection,
featureProjection: mainLizmap.projection
});
} else if (format === "wkt") {
olFeatures = (new WKT()).readFeatures(features, {
dataProjection: projection,
featureProjection: mainLizmap.projection
});
} else {
return;
}
this._highlightLayer.getSource().addFeatures(olFeatures);
}
/**
* Set highlight features on top of all layer
* @param {string} features features as GeoJSON or WKT
* @param {string} format format string as `geojson` or `wkt`
* @param {string|undefined} projection optional features projection
*/
setHighlightFeatures(features, format, projection){
this.clearHighlightFeatures();
this.addHighlightFeatures(features, format, projection);
}
/**
* Clear all highlight features
*/
clearHighlightFeatures() {
this._highlightLayer.getSource().clear();
}
/**
* Synchronize new OL view with OL2 one
* @memberof Map
*/
syncNewOLwithOL2View(){
const center = mainLizmap.lizmap3.map.getCenter()
this.getView().animate({
center: [center.lon, center.lat],
zoom: mainLizmap.lizmap3.map.getZoom(),
duration: 50
});
}
refreshOL2View() {
// This refresh OL2 view and layers
mainLizmap.lizmap3.map.setCenter(
this.getView().getCenter(),
this.getView().getZoom()
);
}
changeBaseLayer(name){
let selectedBaseLayer;
// Choosen base layer is visible, others not
this.baseLayersGroup.getLayers().forEach( baseLayer => {
if (baseLayer.get('name') === name) {
selectedBaseLayer = baseLayer;
baseLayer.set("visible", true, true);
} else {
baseLayer.set("visible", false, true);
}
});
this._baseLayersGroup.changed();
// If base layer projection is different from project projection
// We must set the project extent to the View to reproject nicely
if (selectedBaseLayer?.getSource().getProjection().getCode() !== mainLizmap.projection) {
this.getView().getProjection().setExtent(mainLizmap.lizmap3.map.restrictedExtent.toArray());
} else {
this.getView().getProjection().setExtent(getProjection(mainLizmap.projection).getExtent());
}
// Trigger legacy event
lizMap.events.triggerEvent("lizmapbaselayerchanged", { 'layer': name });
// Refresh metadatas if sub-dock is visible
if ( document.getElementById('sub-dock').offsetParent !== null ) {
lizMap.events.triggerEvent("lizmapswitcheritemselected", {
'name': name, 'type': 'baselayer', 'selected': true
});
}
}
getActiveBaseLayer(){
return this._baseLayersGroup.getLayers().getArray().find(
layer => layer.getVisible()
);
}
/**
* Return overlay layer if `name` matches.
* `name` is unique for every layers
* @param {string} name The layer name.
* @returns {ImageLayer|undefined} The OpenLayers layer or undefined
*/
getLayerByName(name){
// if the layer is included in the singleWMSLayer, return the single ImageLayer instance
if(this._statesSingleWMSLayers.get(name)){
return this._singleImageWmsGroup.getLayersArray()[0]
}
return this.overlayLayers.find(
layer => layer.get('name') === name
);
}
/**
* Return overlay layer or group if `name` matches.
* `name` is unique for every layers/groups
* @param {string} name The layer or group name.
* @returns {ImageLayer|LayerGroup|undefined} The OpenLayers layer or OpenLayers group or undefined
*/
getLayerOrGroupByName(name){
return this.overlayLayersAndGroups.find(
layer => layer.get('name') === name
);
}
/**
* Return MapLayerState instance of WMS layer or group if the layer is loaded in the single WMS image, undefined if not.
* @param {string} name the WMS layer or group name
* @returns {MapLayerState|undefined} the MapLayerState instance of WMS layer or group if the layer is loaded in the single WMS image or undefined.
*/
isSingleWMSLayer(name){
return this.statesSingleWMSLayers.get(name);
}
/**
* Activate DragZoom interaction
*/
activateDragZoom() {
this._dragZoom.setActive(true);
mainEventDispatcher.dispatch('dragZoom.activated');
}
/**
* Deactivate DragZoom interaction
*/
deactivateDragZoom() {
this._dragZoom.setActive(false);
mainEventDispatcher.dispatch('dragZoom.deactivated');
}
/**
* Adds the given layer to the top of the tools group layers.
* @param {LayerBase} layer Layer.
*/
addToolLayer(layer) {
this._toolsGroup.getLayers().push(layer);
}
/**
* Removes the given layer from the tools group layers.
* @param {LayerBase} layer Layer.
*/
removeToolLayer(layer) {
this._toolsGroup.getLayers().remove(layer);
}
/**
* Zoom to given geometry or extent
* @param {geometry|extent} geometryOrExtent The geometry or extent to zoom to. CRS is 4326 by default.
* @param {object} [options] Options.
*/
zoomToGeometryOrExtent(geometryOrExtent, options) {
const geometryType = geometryOrExtent.getType?.();
if (geometryType && (mainLizmap.initialConfig.options.max_scale_lines_polygons || mainLizmap.initialConfig.options.max_scale_lines_polygons)) {
let maxScale;
if (['Polygon', 'Linestring', 'MultiPolygon', 'MultiLinestring'].includes(geometryType)){
maxScale = mainLizmap.initialConfig.options.max_scale_lines_polygons;
} else if (geometryType === 'Point'){
maxScale = mainLizmap.initialConfig.options.max_scale_points;
}
const resolution = mainLizmap.utils.getResolutionFromScale(
maxScale,
mainLizmap.map.getView().getProjection().getMetersPerUnit()
);
if (!options?.minResolution) {
if (!options) {
options = { minResolution: resolution };
} else {
options.minResolution = resolution;
}
}
}
this.getView().fit(geometryOrExtent, options);
}
/**
* Zoom to given feature id
* @param {string} featureTypeDotId The string as `featureType.fid` to zoom to.
* @param {object} [options] Options.
*/
zoomToFid(featureTypeDotId, options) {
const [featureType, fid] = featureTypeDotId.split('.');
if (!featureType || !fid) {
console.log('Wrong string for featureType.fid');
return;
}
lizMap.getLayerFeature(featureType, fid, feat => {
const olFeature = (new GeoJSON()).readFeature(feat, {
dataProjection: 'EPSG:4326',
featureProjection: mainLizmap.projection
});
this.zoomToGeometryOrExtent(olFeature.getGeometry(), options);
});
}
}