UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

572 lines (571 loc) 25.2 kB
import DrawingFeature, { DrawingShape } from './drawingFeature'; import StateManager from '../../tools/state/statemanager'; import { Collection, Feature } from 'ol'; import { Geometry, LineString, Point, Polygon, Circle as CircleGeom, MultiPoint, MultiLineString, LinearRing, MultiPolygon } from 'ol/geom'; import { createBox, createRegularPolygon } from 'ol/interaction/Draw'; import { Style, Stroke, Text, Fill, RegularShape, Circle } from 'ol/style'; import { Modify, Snap, Draw } from 'ol/interaction'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; import GeoJSON from 'ol/format/GeoJSON'; import { never, noModifierKeys, primaryAction } from 'ol/events/condition'; import ConfigManager from '../../tools/configuration/configmanager'; import MapManager from '../../tools/state/mapManager'; import UserInteractionManager from '../../tools/state/userInteractionManager'; import { getDistance, getArea } from '../../tools/utils/olutils'; import { ContextMenu } from '../map/tools/contextmenu'; import { formatCoordinates } from '../../tools/geometrytools'; import { v4 as uuidv4 } from 'uuid'; import { isAlternateMouseClick, isPrimaryPointerAction } from '../../tools/state/userinteractionevent'; function getHalfPoint(coordinates) { return new Point(new LineString(coordinates).getCoordinateAt(0.5)); } function fixLastLength(length, coordinates, scale = 1) { const coord = coordinates; if (coord.length > 1 && length > 0) { const lastLine = [coord[coord.length - 2], coord[coord.length - 1]]; coord[coord.length - 1] = new LineString(lastLine).getCoordinateAt(length / (getDistance(lastLine) * scale)); } } function extractVerticesFromGeometry(geometry) { let vertices = []; // Extract coordinates depending on coordinate array depth if (geometry instanceof Point) { vertices = [geometry.getCoordinates()]; } else if (geometry instanceof MultiPoint || geometry instanceof LineString || geometry instanceof LinearRing) { vertices = geometry.getCoordinates(); } else if (geometry instanceof Polygon || geometry instanceof MultiLineString) { vertices = geometry.getCoordinates().flat(); } else if (geometry instanceof MultiPolygon) { vertices = geometry .getCoordinates() .flat() .map((coordinateList) => coordinateList.flat()); } return new MultiPoint(vertices); } export default class OlDrawing { constructor(map, toolName) { Object.defineProperty(this, "map", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "toolName", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "state", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "configManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "userInteractionManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "drawingSource", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "modifiableFeatures", { enumerable: true, configurable: true, writable: true, value: new Collection([]) }); Object.defineProperty(this, "draw", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "modify", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "snap", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "editContextMenu", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "currentShape", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "featuresMap", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "fixedLength", { enumerable: true, configurable: true, writable: true, value: 0 }); this.map = map; this.toolName = toolName; this.state = StateManager.getInstance().state; this.configManager = ConfigManager.getInstance(); this.userInteractionManager = UserInteractionManager.getInstance(); this.drawingSource = new VectorSource({ features: new Collection() }); this.drawingSource.on('addfeature', (e) => this.onFeatureAdded(e)); this.map.olMap.addLayer(new VectorLayer({ source: this.drawingSource, zIndex: 1001, properties: { addToPrintedLayers: true, altitudeMode: 'clampToGround' } })); this.map.stateManager.subscribe('extendedState.drawing.activeTool', (_oldTool, newTool) => newTool === null ? this.deactivateDrawTool() : this.activateDrawTool(newTool)); // OlCesium duplicates drawn shapes when 3D view is open if its eventListener is not removed StateManager.getInstance().subscribe('globe.loaded', () => { if (this.state.globe.loaded) { this.drawingSource .getListeners('addfeature') ?.forEach((l) => this.drawingSource.removeEventListener('addfeature', l)); this.drawingSource.on('addfeature', (e) => this.onFeatureAdded(e)); } }); this.addSnapInteraction(); this.modify = new Modify({ features: this.modifiableFeatures, // Feature editing is triggered by: 1) primary action = click or touch, 2) alternate mouse click = remove vertices // If another tool is exclusively modifying, this interaction will be prevented from reacting via canExecute() condition: (e) => (isPrimaryPointerAction(e) || isAlternateMouseClick(e)) && this.canExecute('map.modify'), deleteCondition: never, insertVertexCondition: primaryAction, style: new DrawingFeature(1, {}, '').getVertexStyle(true), snapToPointer: true, pixelTolerance: this.map.pixelTolerance }); this.map.olMap.addInteraction(this.modify); this.createEditContextMenu(); this.setInteractionsActive(false); } createEditContextMenu() { this.removeEditContextMenu(); const menuEntries = [ { entry: 'Remove vertex', callback: (_evt, mapCoordinate) => { const successful = this.removeLastInteractedVertex(); if (!successful) { const errorMessage = `It's not possible to remove vertex at ${formatCoordinates(mapCoordinate, this.configManager.Config.general.locale)}`; this.state.infobox.elements.push({ id: uuidv4(), text: errorMessage, type: 'warning' }); } } } ]; const conditionToOpen = (_evt, mapCoordinate) => { if (!this.modify?.getActive() || this.modifiableFeatures.getArray().length === 0) { return false; } // Only proceed if there is an editable vertex under the mouse pointer return this.hasEditableVertexAtCoordinate(mapCoordinate); }; this.editContextMenu = new ContextMenu(menuEntries, true, conditionToOpen); } setInteractionsActive(active) { this.draw?.setActive(active); this.modify?.setActive(active); this.snap?.setActive(active); this.editContextMenu?.setActive(active); } hasEditableVertexAtCoordinate(coordinate) { /* Check if there is a vertex of a selected (=editable) feature within the pixel tolerance of the clicked coordinates. */ const filter = (f) => this.modifiableFeatures.getArray().some((editFeature) => f == editFeature); // Use filter to only query currently selected features const closestElements = this.getClosestVertexAndFeature(coordinate, filter); if (closestElements) { const closestVertex = closestElements[0]; const vertexAsPixel = this.map.olMap.getPixelFromCoordinate(closestVertex); const clickAsPixel = this.map.olMap.getPixelFromCoordinate(coordinate); const dx = vertexAsPixel[0] - clickAsPixel[0]; const dy = vertexAsPixel[1] - clickAsPixel[1]; const distanceToVertex = Math.sqrt(dx * dx + dy * dy); if (distanceToVertex < this.map.pixelTolerance) { return true; } } return false; } getClosestVertexAndFeature(coordinate, filter = () => true) { /* Returns the closest vertex and feature from the drawing source. Feature source can be pre-filtered via filter function. */ const feature = this.drawingSource.getClosestFeatureToCoordinate(coordinate, filter); const geometry = feature?.getGeometry(); if (geometry) { const vertices = extractVerticesFromGeometry(geometry); const closestVertex = vertices.getClosestPoint(coordinate); return [closestVertex, feature]; } return undefined; } removeLastInteractedVertex() { /* Deletes the vertex that the user interacted with last via the modify interaction. Handled events are defined by the modify option properties 'condition' and 'insertVertexCondition'. */ return this.modify?.removePoint(); } addFeatures(features) { features.forEach((feature) => { let olFeature = this.featuresMap.get(feature.id)?.feature; if (olFeature == undefined) { olFeature = this.createOlFeature(feature); this.featuresMap.set(feature.id, { feature: olFeature, shape: feature.type }); this.drawingSource.addFeature(olFeature); } feature.onChange = (df) => olFeature.setStyle((f) => this.getStyle(df, f)); feature.onChange(feature); }); // Refresh the snap interaction this.addSnapInteraction(); } deleteFeatures(features) { features.forEach((f) => { const toRemove = this.featuresMap.get(f.id)?.feature; if (toRemove != undefined) { this.drawingSource.removeFeature(toRemove); this.featuresMap.delete(f.id); } }); } updateModifiableFeatures(features) { // Restricts the modify interaction to the currently selected drawing features this.modifiableFeatures.clear(); features.forEach((df) => { const olFeature = this.featuresMap.get(df.id)?.feature; if (olFeature) { this.modifiableFeatures.push(olFeature); } }); } onFeatureAdded(e) { if (e.feature && this.currentShape !== null) { // If the shape is not in the state already if (!Array.from(this.featuresMap.values()) .map((x) => x.feature) .includes(e.feature)) { let geoJson = {}; // GeoJson does not support disks, so we create our own definition if (this.currentShape == DrawingShape.Disk) { const disk = e.feature.getGeometry(); geoJson = { type: 'Feature', geometry: { type: 'Disk', center: disk.getCenter(), radius: disk.getRadius() } }; } else { geoJson = JSON.parse(new GeoJSON().writeFeature(e.feature)); } const feature = new DrawingFeature(this.currentShape, geoJson); this.featuresMap.set(feature.id, { feature: e.feature, shape: feature.type }); feature.addToState(); } } } createOlFeature(feature) { const geometry = feature.geojson.geometry; let olFeature; if (geometry.type == 'Disk') { olFeature = new Feature(new CircleGeom(geometry.center, geometry.radius)); } else { olFeature = new Feature(new GeoJSON().readFeatures(feature.geojson)[0].getGeometry()); } olFeature.setStyle((f) => this.getStyle(feature, f)); return olFeature; } setFixedLength(length) { this.fixedLength = Number.isNaN(length) ? 0 : length; } createLineStringFixedLength(coordinates, geom) { fixLastLength(this.fixedLength, coordinates); geom = geom ?? new LineString(coordinates); geom.setCoordinates(coordinates); return geom; } createSquareFixedLength(coordinates, geom, proj) { fixLastLength(this.fixedLength, coordinates, Math.SQRT2); return createRegularPolygon(4)(coordinates, geom, proj); } createPolygonFixedLength(coordinates, geom) { const coord = coordinates[0]; fixLastLength(this.fixedLength, coord); geom = geom ?? new Polygon([coord]); geom.setCoordinates([coord]); return geom; } createDiskFixedLength(coordinates, geom) { const coord = coordinates; fixLastLength(this.fixedLength, coord); geom = geom ?? new CircleGeom(coord[0], getDistance(coord)); geom.setCenterAndRadius(coord[0], getDistance(coord)); return geom; } activateDrawTool(tool) { this.deactivateDrawTool(); // Block feature selection while drawing by registering 'map.select' exclusively this.userInteractionManager.registerListener('map.select', true, this.toolName); this.currentShape = tool; let geomFunction = undefined; let olTool; switch (tool) { case DrawingShape.Point: olTool = 'Point'; break; case DrawingShape.Polyline: olTool = 'LineString'; geomFunction = this.createLineStringFixedLength.bind(this); break; case DrawingShape.Polygon: olTool = 'Polygon'; geomFunction = this.createPolygonFixedLength.bind(this); break; case DrawingShape.Disk: olTool = 'Circle'; geomFunction = this.createDiskFixedLength.bind(this); break; case DrawingShape.Square: olTool = 'Circle'; geomFunction = this.createSquareFixedLength.bind(this); break; case DrawingShape.Rectangle: olTool = 'Circle'; geomFunction = createBox(); break; case DrawingShape.FreehandPolyline: olTool = 'LineString'; break; case DrawingShape.FreehandPolygon: olTool = 'Polygon'; break; } this.draw = new Draw({ source: this.drawingSource, type: olTool, freehand: tool == DrawingShape.FreehandPolyline || tool == DrawingShape.FreehandPolygon, stopClick: true, geometryFunction: geomFunction, // Default condition for ol drawing is noModifierKeys(e) // canExecute: If another tool is exclusively drawing, this interaction will be prevented from reacting condition: (e) => noModifierKeys(e) && this.canExecute('map.draw'), style: (f) => this.getStyle(new DrawingFeature(tool, {}, ''), f) }); this.draw.on('drawend', () => { this.draw?.removeLastPoint(); this.draw?.finishDrawing(); }); this.map.olMap.addInteraction(this.draw); this.addSnapInteraction(); } deactivateDrawTool() { if (this.draw) { this.map.olMap.removeInteraction(this.draw); } this.userInteractionManager.unregisterListener('map.select', this.toolName); } addSnapInteraction() { if (this.snap) { this.map.olMap.removeInteraction(this.snap); } this.snap = new Snap({ source: this.drawingSource, pixelTolerance: this.map.pixelTolerance }); this.map.olMap.addInteraction(this.snap); } centerViewOnFeature(feature) { const olFeature = this.featuresMap.get(feature.id)?.feature; const extent = olFeature?.getGeometry()?.getExtent(); if (extent) { const minResolution = ConfigManager.getInstance().Config.search.minResolution; MapManager.getInstance().zoomToExtent(extent, minResolution); } } // TODO Move as much parameters as possible into DrawingFeature getStyle(feature, olFeature) { if (feature == null) { const shape = Array.from(this.featuresMap.values()).filter((x) => x.feature == olFeature)[0].shape; feature = new DrawingFeature(shape, {}, ''); } const geometry = olFeature.getGeometry(); const measureFont = 'Bold ' + feature.measureFontSize + 'px/1 ' + feature.font; const nameFont = 'Bold ' + feature.nameFontSize + 'px/1 ' + feature.font; const measureColor = 'rgba(0, 0, 0, 0.4)'; const defaultStyle = new Style({ stroke: new Stroke({ color: feature.strokeColor, width: feature.strokeWidth }), fill: new Fill({ color: feature.fillColor }), image: new Circle({ radius: feature.strokeWidth, // Points are using default stroke parameters fill: new Fill({ color: feature.strokeColor }) }), text: new Text({ text: feature.displayName ? feature.name : '', font: nameFont, textBaseline: 'bottom', offsetY: feature.type == DrawingShape.Point ? 2 * feature.nameFontSize : 1.2 * feature.nameFontSize, fill: new Fill({ color: feature.nameColor }) }) }); const labelStyle = new Style({ text: new Text({ font: measureFont, padding: [2, 2, 2, 2], textBaseline: 'bottom', offsetY: -1 * feature.nameFontSize, fill: new Fill({ color: feature.measureColor }) }), image: new RegularShape({ radius: 6, points: 3, angle: Math.PI, displacement: [0, 8], fill: new Fill({ color: measureColor }) }) }); const styles = [defaultStyle]; const addLabel = (position, text) => { if (text != '') { const style = labelStyle.clone(); style.setGeometry(position); style.getText().setText(text); styles.push(style); } }; // If the shape is being constructed (ex. it is a polygon for which only two points are placed yet) if (geometry.getType() == 'LineString' && feature.type !== DrawingShape.Polyline && feature.type !== DrawingShape.FreehandPolyline) { return []; } if (feature.type == DrawingShape.Point || geometry.getType() === 'Point') { addLabel(geometry, feature.getCoordText(geometry.getCoordinates())); } else if (feature.type == DrawingShape.Polyline) { geometry.forEachSegment((a, b) => addLabel(getHalfPoint([a, b]), feature.getLengthText(getDistance([a, b])))); } else if (feature.type == DrawingShape.Polygon) { const polygon = geometry; const segments = this.ensurePolygonIsProperlyClosed(polygon); new LineString(segments).forEachSegment((a, b) => addLabel(getHalfPoint([a, b]), feature.getLengthText(getDistance([a, b])))); addLabel(polygon.getInteriorPoint(), feature.getAreaText(getArea(polygon))); } else if (feature.type == DrawingShape.Disk) { const radius = geometry.getRadius(); const center = geometry.getCenter(); const radiusLine = [center, [center[0] + radius, center[1]]]; const radiusLineStyle = defaultStyle.clone(); radiusLineStyle.setStroke(new Stroke({ color: measureColor, width: feature.strokeWidth })); radiusLineStyle.getText().setText(''); radiusLineStyle.setGeometry(feature.displayMeasure ? new LineString(radiusLine) : new LineString([])); styles.push(radiusLineStyle); addLabel(getHalfPoint(radiusLine), feature.getLengthText(radius)); } else if (feature.type == DrawingShape.FreehandPolygon) { const polygon = geometry; this.ensurePolygonIsProperlyClosed(polygon); addLabel(polygon.getInteriorPoint(), feature.getAreaText(getArea(polygon))); addLabel(new Point(polygon.getCoordinates()[0][0]), feature.getLengthText(getDistance(polygon.getCoordinates()[0]))); } else if (feature.type == DrawingShape.FreehandPolyline) { const line = geometry; addLabel(new Point(line.getCoordinates()[0]), feature.getLengthText(getDistance(line.getCoordinates()))); } else if (feature.type == DrawingShape.Rectangle) { const rect = geometry; const segment1 = [rect.getCoordinates()[0][0], rect.getCoordinates()[0][1]]; const segment2 = [rect.getCoordinates()[0][1], rect.getCoordinates()[0][2]]; addLabel(getHalfPoint(segment1), feature.getLengthText(getDistance(segment1))); addLabel(getHalfPoint(segment2), feature.getLengthText(getDistance(segment2))); addLabel(rect.getInteriorPoint(), feature.getAreaText(getArea(rect))); } else if (feature.type == DrawingShape.Square) { const square = geometry; const segment = [square.getCoordinates()[0][0], square.getCoordinates()[0][1]]; addLabel(getHalfPoint(segment), feature.getLengthText(getDistance(segment))); addLabel(square.getInteriorPoint(), feature.getAreaText(getArea(square))); } if (feature.selected) { const vertexStyle = feature.getVertexStyle(); // Add a node style to every vertex of the geometry vertexStyle.setGeometry(function (f) { const geom = f?.getGeometry(); if (geom && geom instanceof Geometry) { return extractVerticesFromGeometry(geom); } }); styles.push(vertexStyle); } return styles; } ensurePolygonIsProperlyClosed(polygon) { const coordinates = polygon.getCoordinates()[0]; let segments = [...coordinates]; if (coordinates.length > 2 && coordinates[0][0] != coordinates[coordinates.length - 1][0]) { segments = [...coordinates, coordinates[0]]; polygon.setCoordinates([segments]); } return segments; } removeEditContextMenu() { if (this.editContextMenu) { this.editContextMenu.remove(); this.editContextMenu = null; } } registerInteractions() { this.userInteractionManager.registerListener('map.draw', true, this.toolName); this.userInteractionManager.registerListener('map.modify', true, this.toolName); this.userInteractionManager.registerListener('map.snap', true, this.toolName); } unregisterInteractions() { this.userInteractionManager.unregisterListener('map.draw', this.toolName); this.userInteractionManager.unregisterListener('map.modify', this.toolName); this.userInteractionManager.unregisterListener('map.snap', this.toolName); } canExecute(event) { return this.userInteractionManager.canListenerExecute(event, this.toolName); } }