UNPKG

@terrestris/ol-util

Version:

A set of helper classes for working with openLayers

446 lines (445 loc) 17.6 kB
import { isNil } from 'lodash'; import findIndex from 'lodash/findIndex'; import _isFinite from 'lodash/isFinite'; import _isNil from 'lodash/isNil'; import _isString from 'lodash/isString'; import OlGeomGeometryCollection from 'ol/geom/GeometryCollection'; import OlLayerGroup from 'ol/layer/Group'; import { toLonLat } from 'ol/proj'; import { METERS_PER_UNIT } from 'ol/proj/Units'; import OlSourceTileWMS from 'ol/source/TileWMS'; import OlSourceWMTS from 'ol/source/WMTS'; import { getUid } from 'ol/util'; import logger from '@terrestris/base-util/dist/Logger'; import UrlUtil from '@terrestris/base-util/dist/UrlUtil/UrlUtil'; import FeatureUtil from '../FeatureUtil/FeatureUtil'; import LayerUtil from '../LayerUtil/LayerUtil'; import { isWmsLayer } from '../typeUtils/typeUtils'; /** * Helper class for the OpenLayers map. * * @class */ export class MapUtil { /** * Returns all interactions by the given name of a map. * * @param {OlMap} map The map to use for lookup. * @param {string} name The name of the interaction to look for. * @return The list of result interactions. */ static getInteractionsByName(map, name) { return map.getInteractions() .getArray() .filter(interaction => interaction.get('name') === name); } /** * Calculates the appropriate map resolution for a given scale in the given * units. * * See: https://gis.stackexchange.com/questions/158435/ * how-to-get-current-scale-in-openlayers-3 * * @method * @param {number|string} scale The input scale to calculate the appropriate * resolution for. * @param {Units} units The units to use for calculation (m or degrees). * @return {number} The calculated resolution. */ static getResolutionForScale(scale, units) { const dpi = 25.4 / 0.28; let mpu; if (units === 'm') { mpu = METERS_PER_UNIT.m; } else if (units === 'degrees') { mpu = METERS_PER_UNIT.degrees; } else { logger.info('Currently only \'degrees\' and \'m\' units are supported.'); return undefined; } const inchesPerMeter = 39.37; return (_isString(scale) ? parseFloat(scale) : scale) / (mpu * inchesPerMeter * dpi); } /** * Returns the appropriate scale for the given resolution and units. * * @method * @param {number|string} resolution The resolutions to calculate the scale for. * @param {string} units The units the resolution is based on, typically * either 'm' or 'degrees'. * @return {number} The appropriate scale. */ static getScaleForResolution(resolution, units) { const dpi = 25.4 / 0.28; let mpu; if (units === 'm') { mpu = METERS_PER_UNIT.m; } else if (units === 'degrees') { mpu = METERS_PER_UNIT.degrees; } else { logger.info('Currently only \'degrees\' and \'m\' units are supported.'); return undefined; } const inchesPerMeter = 39.37; return (_isString(resolution) ? parseFloat(resolution) : resolution) * mpu * inchesPerMeter * dpi; } /** * Returns all layers of a collection. Even the hidden ones. * * @param {OlMap | OlLayerGroup} collection The collection to get the layers * from. This can be an ol.layer.Group * or an ol.Map. * @param {(olLayer: OlBaseLayer) => boolean} [filter] A filter function that receives the layer. * If it returns true it will be included in the * returned layers. * @return {OlBaseLayer} An array of all Layers. */ static getAllLayers(collection, filter = () => true) { const layers = collection.getLayers().getArray(); return layers.flatMap((layer) => { let ll = []; if (layer instanceof OlLayerGroup) { ll = MapUtil.getAllLayers(layer, filter); } if (filter(layer)) { ll.push(layer); } return ll; }); } /** * Get a layer by its key (ol_uid). * * @param {OlMap} map The map to use for lookup. * @param olUid * @return {OlBaseLayer|undefined} The layer. */ static getLayerByOlUid = (map, olUid) => { return MapUtil.getAllLayers(map, l => olUid === getUid(l).toString())[0]; }; /** * Returns the layer from the provided map by the given name. * * @param {OlMap} map The map to use for lookup. * @param {string} name The name to get the layer by. * @return {OlBaseLayer} The result layer or undefined if the layer could not * be found. */ static getLayerByName(map, name) { return MapUtil.getAllLayers(map, l => l.get('name') === name)[0]; } /** * Returns the layer from the provided map by the given name * (parameter LAYERS). * * @param {OlMap} map The map to use for lookup. * @param {string} name The name to get the layer by. * @return {WmsLayer|undefined} * The result layer or undefined if the layer could not be found. */ static getLayerByNameParam(map, name) { const layer = MapUtil.getAllLayers(map, l => { return isWmsLayer(l) && l.getSource()?.getParams().LAYERS === name; })[0]; return layer; } /** * Returns the layer from the provided map by the given feature. * * @param {OlMap} map The map to use for lookup. * @param {OlFeature<OlGeometry>} feature The feature to get the layer by. * @param {string[]} namespaces list of supported GeoServer namespaces. * @return {OlBaseLayer|undefined} The result layer or undefined if the layer could not * be found. */ static getLayerByFeature(map, feature, namespaces) { const featureTypeName = FeatureUtil.getFeatureTypeName(feature); for (const namespace of namespaces) { const qualifiedFeatureTypeName = `${namespace}:${featureTypeName}`; const layer = MapUtil.getLayerByNameParam(map, qualifiedFeatureTypeName); if (layer) { return layer; } } return undefined; } /** * Returns all layers of the specified layer group recursively. * * @param {OlMap} map The map to use for lookup. * @param {OlLayerGroup} layerGroup The group to flatten. * @return {OlBaseLayer} The (flattened) layers from the group */ static getLayersByGroup(map, layerGroup) { return layerGroup.getLayers().getArray() .flatMap((layer) => { if (layer instanceof OlLayerGroup) { return MapUtil.getLayersByGroup(map, layer); } else { return [layer]; } }); } /** * Returns the list of layers matching the given pair of properties. * * @param {OlMap} map The map to use for lookup. * @param {string} key The property key. * @param {any} value The property value. * * @return {OlBaseLayer[]} The array of matching layers. */ static getLayersByProperty(map, key, value) { return MapUtil.getAllLayers(map, l => l.get(key) === value); } /** * Get information about the LayerPosition in the tree. * * @param {OlBaseLayer} layer The layer to get the information. * @param {OlLayerGroup|OlMap} groupLayerOrMap The groupLayer or map * containing the layer. * @return {{ * groupLayer: OlLayerGroup, * position: number * }} The groupLayer containing the layer and the position of the layer in the collection. */ static getLayerPositionInfo(layer, groupLayerOrMap) { const groupLayer = groupLayerOrMap instanceof OlLayerGroup ? groupLayerOrMap : groupLayerOrMap.getLayerGroup(); const layers = groupLayer.getLayers().getArray(); let info = {}; if (layers.indexOf(layer) < 0) { layers.forEach((childLayer) => { if (childLayer instanceof OlLayerGroup && !info.groupLayer) { info = MapUtil.getLayerPositionInfo(layer, childLayer); } }); } else { info.position = layers.indexOf(layer); info.groupLayer = groupLayer; } return info; } /** * Get the getlegendGraphic url of a layer. Designed for geoserver. * Currently supported Sources: * - ol.source.TileWms (with url configured) * - ol.source.ImageWms (with url configured) * - ol.source.WMTS (with url configured) * * @param {WmsLayer | WmtsLayer} layer The layer that you want to have a legendUrl for. * @param {Object} extraParams * @return {string} The getLegendGraphicUrl. */ static getLegendGraphicUrl(layer, extraParams = {}) { const source = layer.getSource(); if (!source) { throw new Error('Layer has no source.'); } if (source instanceof OlSourceWMTS) { return source.get('legendUrl') ? source.get('legendUrl') : ''; } else { const url = (source instanceof OlSourceTileWMS ? source.getUrls()?.[0] : source.getUrl()) ?? ''; const params = { LAYER: source.getParams().LAYERS, VERSION: '1.3.0', SERVICE: 'WMS', REQUEST: 'GetLegendGraphic', FORMAT: 'image/png' }; const queryString = UrlUtil.objectToRequestString(Object.assign(params, extraParams)); return /\?/.test(url) ? `${url}&${queryString}` : `${url}?${queryString}`; } } /** * Checks whether the resolution of the passed map's view lies inside of the * min- and max-resolution of the passed layer, e.g. whether the layer should * be displayed at the current map view resolution. * * @param {OlBaseLayer} layer The layer to check. * @param {OlMap} map The map to get the view resolution for comparison * from. * @return {boolean} Whether the resolution of the passed map's view lies * inside of the min- and max-resolution of the passed layer, e.g. whether * the layer should be displayed at the current map view resolution. Will * be `false` when no `layer` or no `map` is passed or if the view of the * map is falsy or does not have a resolution (yet). */ static layerInResolutionRange(layer, map) { const mapView = map?.getView(); const currentRes = mapView?.getResolution(); if (isNil(layer) || !mapView || !currentRes) { // It is questionable what we should return in this case, I opted for // false, since we cannot sanely determine a correct answer. return false; } const layerMinRes = layer.getMinResolution(); // default: 0 if unset const layerMaxRes = layer.getMaxResolution(); // default: Infinity if unset // minimum resolution is inclusive, maximum resolution exclusive return currentRes >= layerMinRes && currentRes < layerMaxRes; } /** * Rounds a scale number depending on its size. * * @param {number} scale The exact scale * @return {number} The roundedScale */ static roundScale(scale) { if (scale < 100) { return Math.round(scale); } if (scale >= 100 && scale < 10000) { return Math.round(scale / 10) * 10; } if (scale >= 10000 && scale < 1000000) { return Math.round(scale / 100) * 100; } // scale >= 1000000 return Math.round(scale / 1000) * 1000; } /** * Returns the appropriate zoom level for the given scale and units. * @method * @param {number} scale Map scale to get the zoom for. * @param {number[]} resolutions Resolutions array. * @param {string} units The units the resolutions are based on, typically * either 'm' or 'degrees'. Default is 'm'. * * @return {number} Determined zoom level for the given scale. */ static getZoomForScale(scale, resolutions, units = 'm') { if (Number.isNaN(Number(scale))) { return 0; } if (scale < 0) { return 0; } const calculatedResolution = MapUtil.getResolutionForScale(scale, units); if (!_isFinite(calculatedResolution)) { throw new Error('Can not determine unit / scale from map'); } const closestVal = resolutions.reduce((prev, curr) => { return Math.abs(curr - calculatedResolution) < Math.abs(prev - calculatedResolution) ? curr : prev; }); return findIndex(resolutions, function (o) { return Math.abs(o - closestVal) <= 1e-10; }); } /** * Fits the map's view to the extent of the passed features. * * @param {OlMap} map The map to get the view from. * @param {OlFeature[]} features The features to zoom to. */ static zoomToFeatures(map, features) { const featGeometries = FeatureUtil.mapFeaturesToGeometries(features); if (featGeometries.length > 0) { const geomCollection = new OlGeomGeometryCollection(featGeometries); map.getView().fit(geomCollection.getExtent()); } } /** * Checks if the given layer is visible for the given resolution. * * @param {OlBaseLayer} layer The layer. * @param {number} resolution The resolution of the map */ static isInScaleRange(layer, resolution) { return resolution >= layer?.get('minResolution') && resolution < layer?.get('maxResolution'); } /** * Converts a given OpenLayers map to an inkmap spec. Only returns options which can be * derived from a map (center, scale, projection, layers). * * @param {OlMap} olMap The ol map. * * @return {Promise<Partial<import("../types").InkmapPrintSpec>>} Promise of the inmkap print spec. */ static async generatePrintConfig(olMap) { const unit = olMap.getView().getProjection().getUnits(); const resolution = olMap.getView().getResolution(); const projection = olMap.getView().getProjection().getCode(); if (resolution === undefined) { throw new Error('Can not determine resolution from map'); } const scale = MapUtil.getScaleForResolution(resolution, unit); const center = olMap?.getView().getCenter(); if (!unit || !center || !_isFinite(scale)) { throw new Error('Can not determine unit / scale from map'); } const centerLonLat = toLonLat(center, projection); const layerPromises = olMap.getAllLayers() .map(LayerUtil.mapOlLayerToInkmap); const responses = await Promise.allSettled(layerPromises); const layers = responses .filter(r => r !== null && r.status === 'fulfilled') .map((l) => l.value); const rejectedLayers = responses .filter(r => r && r.status === 'rejected'); rejectedLayers.forEach(r => logger.warn('A layer could not be printed, maybe its invisible or unsupported: ', r)); // ignore typecheck because responses.filter(l => l !== null) is not recognized properly return { layers: layers, center: centerLonLat, scale: scale, projection: projection }; } /** * Set visibility for layer having names (if in map) * @param {OlMap} olMap The OpenLayers map. * @param {string[]} layerNames An array of layer names (feature type names can also be used) * @param {boolean} visible if layer should be visible or not */ static setVisibilityForLayers(olMap, layerNames, visible) { if (_isNil(olMap)) { return; } if (_isNil(layerNames) || layerNames.length === 0) { return; } layerNames?.forEach(layerName => { let layer = MapUtil.getLayerByName(olMap, layerName); if (_isNil(layer)) { layer = MapUtil.getLayerByNameParam(olMap, layerName); } layer?.setVisible(visible); }); } static calculateScaleAndCenterForExtent(olMap, extent) { if (_isNil(olMap) || _isNil(extent) || extent.length !== 4) { return; } const view = olMap.getView(); const resolution = view.getResolutionForExtent(extent); const unit = view.getProjection().getUnits(); const scale = MapUtil.getScaleForResolution(resolution, unit); const center = [ (extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2 ]; if (_isNil(unit) || _isNil(center) || _isNil(scale) || !_isFinite(scale)) { logger.error('Can not determine unit / scale from map'); return; } return { center, scale }; } } export default MapUtil; //# sourceMappingURL=MapUtil.js.map