@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
238 lines (237 loc) • 10.9 kB
JavaScript
// 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;