UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

238 lines (237 loc) 10.9 kB
// SPDX-License-Identifier: Apache-2.0 import GirafeSingleton from '../../base/GirafeSingleton.js'; import { Feature } from 'ol'; import DragAndDrop from 'ol/interaction/DragAndDrop.js'; import { GPX, GeoJSON, IGC, KML, TopoJSON } from 'ol/format.js'; import { Vector as VectorLayer } from 'ol/layer.js'; import { Vector as VectorSource } from 'ol/source.js'; import { Circle, Fill, Stroke, Style } from 'ol/style.js'; import LayerLocalFile from '../../models/layers/layerlocalfile.js'; import { extend, intersects } from 'ol/extent.js'; import { applyFeaturesToSelection } from '../utils/utils.js'; import DrawingFeature, { DrawingShape } from '../drawing/drawingFeature.js'; const DrawingStateLocation = 'drawing'; class LocalFileManager extends GirafeSingleton { map; name; supportedFileFormats = [GPX, GeoJSON, IGC, new KML({ extractStyles: true }), TopoJSON]; supportedFileExtensions = ['gpx', 'geojson', 'igc', 'kml', 'topojson', 'json']; activeLayers = {}; // Keeps track of which LayerLocalFile a given feature belongs to, so its layer name can be // used as a display fallback when the feature has no WFS-style id featureLayerMap = new WeakMap(); constructor(context) { super(context); this.map = this.context.mapManager.getMap(); this.name = `localFileManager`; } initializeSingleton() { this.registerEvents(); // Add drag n drop interaction to add local files const dragAndDropInteraction = this.createInteraction(); this.map.addInteraction(dragAndDropInteraction); } registerEvents() { this.context.userInteractionManager.registerListener('map.drop', false, this.name); } createInteraction() { // Handle dropping of unsupported files this.map.getViewport().addEventListener('drop', (event) => this.handleUnsupportedFiles(event)); const dragAndDropInteraction = new DragAndDrop({ // @ts-expect-error ol Format types formatConstructors: this.supportedFileFormats }); dragAndDropInteraction.on('addfeatures', (e) => { if (!this.context.userInteractionManager.canListenerExecute('map.drop', this.name)) return; this.loadLocalFileFeatures(e.file, e.features); }); return dragAndDropInteraction; } async loadLocalFile(localFile) { const text = await localFile.text(); let reader; if (text.includes('<kml') && text.includes('</kml>')) { reader = new KML({ extractStyles: true }); } else if (text.includes('<gpx') && text.includes('</gpx>')) { reader = new GPX(); } else if (text.startsWith('{') && text.endsWith('}')) { reader = new GeoJSON(); } else { // dot nothing - shall we report an error ?? return; } const features = reader.readFeatures(text, { featureProjection: this.context.stateManager.state.projection }); this.loadLocalFileFeatures(localFile, features); } loadLocalFileFeatures(localFile, features) { // Check if all features can be displayed in the current map maximum extent // This will also approximately validate if the SRID is correct const featureType = localFile.name.replace('.', '_'); const acceptableFeatures = this.validateAndCompleteFeatures(featureType, features); // Create Layer if (acceptableFeatures.globalExtent === null) { const title = this.context.i18nManager.getTranslation('No features within map extent'); const msg = this.context.i18nManager.getTranslation('No features where found in your file that could be displayed within the maximal extent configured in your application.'); window.gAlert(msg, title); return; } const layer = new LayerLocalFile(0, 0, localFile, acceptableFeatures.features, acceptableFeatures.globalExtent, this.context.configManager.Config.general.locale); if (features && features.length > acceptableFeatures.features.length) { // Some features are outer extent layer.hasError = true; layer.errorMessage = this.context.i18nManager .getTranslation('only-n-features-loaded') .replace('_loadedCount_', String(acceptableFeatures.features.length)) .replace('_totalCount_', String(features.length)); } this.context.userLayerManager.addUserLayerToTree(layer); } convertToDrawing(layer) { const drawingState = this.context.stateManager.state.extendedState[DrawingStateLocation]; for (const feature of layer._features) { const geometry = feature.getGeometry(); if (!geometry) continue; for (const { geometry: simpleGeometry, shape } of this.splitIntoSimpleGeometries(geometry)) { const drawingFeature = DrawingFeature.createFromOlFeature(shape, new Feature(simpleGeometry), this.context, drawingState); drawingFeature.displayName = false; drawingFeature.measureInformation = 'none'; } } this.context.userLayerManager.removeUserLayerFromTree(layer); } splitIntoSimpleGeometries(geometry) { switch (geometry.getType()) { case 'Point': return [{ geometry, shape: DrawingShape.Point }]; case 'LineString': return [{ geometry, shape: DrawingShape.Polyline }]; case 'Polygon': return [{ geometry, shape: DrawingShape.Polygon }]; case 'MultiPoint': return geometry.getPoints().map((g) => ({ geometry: g, shape: DrawingShape.Point })); case 'MultiLineString': return geometry .getLineStrings() .map((g) => ({ geometry: g, shape: DrawingShape.Polyline })); case 'MultiPolygon': return geometry.getPolygons().map((g) => ({ geometry: g, shape: DrawingShape.Polygon })); case 'GeometryCollection': return geometry.getGeometries().flatMap((g) => this.splitIntoSimpleGeometries(g)); default: // Unsupported geometry type return []; } } handleUnsupportedFiles(dropEvent) { if (!this.context.userInteractionManager.canListenerExecute('map.drop', this.name)) return; const files = dropEvent.dataTransfer?.files; if (!files?.length) { return; } const unsupportedFiles = Array.from(files).filter((file) => !this.supportedFileExtensions.includes(file.name.split('.').at(-1).toLowerCase())); if (!unsupportedFiles?.length) { return; } let msg; if (unsupportedFiles.length > 1) { msg = this.context.i18nManager.getTranslation('Files _fileNames_ are not supported'); msg = msg.replace('_fileNames_', unsupportedFiles.map((f) => `"${f.name}"`).join(', ')); } else { msg = this.context.i18nManager.getTranslation('File _fileName_ is not supported'); msg = msg.replace('_fileName_', `"${unsupportedFiles[0].name}"`); } void window.gAlert(msg, 'Unsupported file format'); } validateAndCompleteFeatures(featureType, features) { const validatedFeatures = []; const maxExtent = this.map.getView().get('extent'); let counter = 0; let globalExtent = null; for (const feature of features) { const geometry = feature.getGeometry(); if (geometry) { const featureExtent = geometry.getExtent(); if (intersects(maxExtent, featureExtent)) { // Set an id that looks like ids from WFS qureries in order to make the usage easy in the others components that understand WFS Features feature.setId(`${featureType}.${++counter}`); validatedFeatures.push(feature); globalExtent = globalExtent == null ? [...featureExtent] : extend(globalExtent, featureExtent); } } } return { features: validatedFeatures, globalExtent: globalExtent }; } buildFeatureStyle() { const drawingConfig = this.context.configManager.Config.drawing; return new Style({ fill: new Fill({ color: drawingConfig.defaultFillColor }), stroke: new Stroke({ color: drawingConfig.defaultStrokeColor, width: drawingConfig.defaultStrokeWidth }), image: new Circle({ radius: 10, fill: new Fill({ color: drawingConfig.defaultFillColor }), stroke: new Stroke({ color: drawingConfig.defaultStrokeColor, width: drawingConfig.defaultStrokeWidth }) }) }); } addLayer(layerFile) { const vectorSource = new VectorSource({ features: layerFile._features }); const olayer = new VectorLayer({ source: vectorSource, style: this.buildFeatureStyle() }); this.map.addLayer(olayer); this.activeLayers[layerFile.treeItemId] = { layerFile: layerFile, olayer: olayer }; layerFile._features.forEach((feature) => this.featureLayerMap.set(feature, layerFile)); } getLayer(layerFile) { if (this.layerExists(layerFile)) { return this.activeLayers[layerFile.treeItemId].olayer; } return null; } removeLayer(layerFile) { if (this.layerExists(layerFile)) { const olayer = this.activeLayers[layerFile.treeItemId].olayer; delete this.activeLayers[layerFile.treeItemId]; this.map.removeLayer(olayer); layerFile._features.forEach((feature) => this.featureLayerMap.delete(feature)); } else { throw new Error('Cannot remove this layer: it does not exist'); } } layerExists(layer) { return layer.treeItemId in this.activeLayers; } /** * Returns the LayerLocalFile a given feature belongs to */ getLayerForFeature(feature) { return this.featureLayerMap.get(feature); } selectFeatures(extent) { for (const activeLayer of Object.values(this.activeLayers)) { const features = activeLayer.olayer.getSource()?.getFeaturesInExtent(extent); if (features && features.length > 0) { applyFeaturesToSelection(features, this.context.stateManager.state); } } } changeOpacity(layer) { const oLayer = this.activeLayers[layer.treeItemId].olayer; oLayer.setOpacity(layer.opacity); } } export default LocalFileManager;