@geogirafe/lib-geoportal 1.1.0-dev.2456794124 → 1.1.0-dev.2463680603

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,6 +33,7 @@ export default class OlDrawing {
33
33
  lastClosestFeature: Feature | null;
34
34
  translate: Translate | null;
35
35
  transform: Transform | null;
36
+ private readonly modifyFeatureChangeListeners;
36
37
  defaultStyle: StyleFunction | undefined;
37
38
  private readonly updateGeometryInState;
38
39
  private get state();
@@ -114,6 +115,12 @@ export default class OlDrawing {
114
115
  getStyle(dFeature: DrawingFeature, olFeature: Feature<Geometry>): Style[];
115
116
  private removeDrawInteraction;
116
117
  private removeModifyInteraction;
118
+ private getDrawingShapeType;
119
+ private isSquareOrRectangle;
120
+ private shouldAllowVertexInsertionAtEvent;
121
+ private getHoveredModifiableFeatureAtPixel;
122
+ private applyScaleRotateOnModifyStyle;
123
+ private clearModifyFeatureChangeListeners;
117
124
  private removeSnapInteraction;
118
125
  private removeEditContextMenu;
119
126
  registerInteractions(): void;
@@ -10,11 +10,12 @@ import VectorLayer from 'ol/layer/Vector.js';
10
10
  import GeoJSON from 'ol/format/GeoJSON.js';
11
11
  import { getPointResolution } from 'ol/proj.js';
12
12
  import { always, never, primaryAction } from 'ol/events/condition.js';
13
- import { ensurePolygonIsProperlyClosed, getAreaOfPolygon, getDistance, getHalfPoint, getLabelStyle, getRadiusDataForCircle } from '../../tools/utils/olutils.js';
13
+ import { ensurePolygonIsProperlyClosed, getAreaOfPolygon, getDistance, getHalfPoint, getLabelStyle, getRadiusDataForCircle, unByKeyAll } from '../../tools/utils/olutils.js';
14
14
  import { ContextMenu, EntryInteractionType } from '../map/tools/contextmenu.js';
15
15
  import { formatCoordinates } from '../../tools/geometrytools.js';
16
16
  import { v4 as uuidv4 } from 'uuid';
17
17
  import { isAlternateMouseClick, isPrimaryPointerAction } from '../../tools/state/userinteractionevent.js';
18
+ import { calculateCenterAndMinRadius, createScaledAndRotatedGeometry, getGeometryForRendering, shouldAllowVertexInsertionForShape } from './shapeConstraints.js';
18
19
  import Transform from 'ol-ext/interaction/Transform.js';
19
20
  function getLineStroke(strokeType, lineWidth) {
20
21
  switch (strokeType) {
@@ -68,6 +69,7 @@ export default class OlDrawing {
68
69
  lastClosestFeature = null;
69
70
  translate = null;
70
71
  transform = null;
72
+ modifyFeatureChangeListeners = [];
71
73
  defaultStyle;
72
74
  updateGeometryInState = (olFeature) => {
73
75
  const idx = this.drawingState.features.findIndex((f) => f.id === olFeature.getId());
@@ -127,6 +129,7 @@ export default class OlDrawing {
127
129
  }
128
130
  addModifyInteraction() {
129
131
  this.removeModifyInteraction();
132
+ const vertexStyle = new DrawingFeature(DrawingShape.Point, this.drawingState, this.config.drawing).getVertexStyle(true);
130
133
  this.modify = new Modify({
131
134
  features: this.modifiableFeatures,
132
135
  // Feature editing is triggered by: 1) primary action = click or touch, 2) alternate mouse click = remove vertex
@@ -135,34 +138,64 @@ export default class OlDrawing {
135
138
  isAlternateMouseClick(e)) &&
136
139
  this.canExecute('map.modify'),
137
140
  deleteCondition: never,
138
- insertVertexCondition: primaryAction,
139
- style: new DrawingFeature(DrawingShape.Point, this.drawingState, this.config.drawing).getVertexStyle(true),
141
+ // For square/rectangle, we disable vertex insertion only on the hovered shape.
142
+ insertVertexCondition: (e) => primaryAction(e) && this.shouldAllowVertexInsertionAtEvent(e),
143
+ style: (feature) => {
144
+ this.applyScaleRotateOnModifyStyle(feature);
145
+ return vertexStyle;
146
+ },
140
147
  snapToPointer: true,
141
148
  pixelTolerance: this.map.pixelTolerance
142
149
  });
143
150
  this.map.olMap.addInteraction(this.modify);
144
151
  this.modify.on('modifystart', (e) => {
152
+ this.clearModifyFeatureChangeListeners();
145
153
  e.features.forEach((olFeature) => {
146
- olFeature.on('change', (e) => {
147
- const geometry = e.target.getGeometry();
148
- if (geometry && geometry.getType() == 'Circle') {
149
- const circle = geometry;
150
- const newRadius = circle.getRadius();
151
- const properties = circle.getProperties();
152
- const { pointer } = properties;
153
- const oldRadiusLine = new LineString([circle.getCenter(), pointer]);
154
- oldRadiusLine.scale(newRadius / oldRadiusLine.getLength());
155
- circle.setProperties({
156
- ...properties,
157
- pointer: oldRadiusLine.getLastCoordinate()
158
- });
159
- }
160
- });
154
+ const geometry = olFeature.getGeometry();
155
+ if (!geometry) {
156
+ return;
157
+ }
158
+ const shapeType = this.drawingState.features.find((f) => f.id === olFeature.getId())?.type;
159
+ if (shapeType === DrawingShape.Square || shapeType === DrawingShape.Rectangle) {
160
+ olFeature.set('modifyGeometry', { geometry: geometry.clone() }, true);
161
+ }
162
+ else if (shapeType === DrawingShape.Disk) {
163
+ const listener = olFeature.on('change', (e) => {
164
+ const geometry = e.target.getGeometry();
165
+ if (!geometry) {
166
+ return;
167
+ }
168
+ if (geometry.getType() === 'Circle') {
169
+ const circle = geometry;
170
+ const newRadius = circle.getRadius();
171
+ const properties = circle.getProperties();
172
+ const { pointer } = properties;
173
+ const oldRadiusLine = new LineString([circle.getCenter(), pointer]);
174
+ oldRadiusLine.scale(newRadius / oldRadiusLine.getLength());
175
+ circle.setProperties({
176
+ ...properties,
177
+ pointer: oldRadiusLine.getLastCoordinate()
178
+ });
179
+ }
180
+ });
181
+ this.modifyFeatureChangeListeners.push(listener);
182
+ }
161
183
  });
162
184
  });
163
185
  // Update the modified geometries in the state
164
186
  this.modify.on('modifyend', (e) => {
165
- e.features.forEach((olFeature) => this.updateGeometryInState(olFeature)); //NOSONAR(typescript:S7728) Collection<?> is a custom Implementation with custom forEach
187
+ this.clearModifyFeatureChangeListeners();
188
+ e.features.forEach((olFeature) => {
189
+ const shapeType = this.drawingState.features.find((f) => f.id === olFeature.getId())?.type;
190
+ if (shapeType === DrawingShape.Square || shapeType === DrawingShape.Rectangle) {
191
+ const modifyGeometry = olFeature.get('modifyGeometry');
192
+ if (modifyGeometry?.geometry) {
193
+ olFeature.setGeometry(modifyGeometry.geometry);
194
+ }
195
+ olFeature.unset('modifyGeometry', true);
196
+ }
197
+ this.updateGeometryInState(olFeature); //NOSONAR(typescript:S7728) Collection<?> is a custom Implementation with custom forEach
198
+ });
166
199
  });
167
200
  }
168
201
  addSnapInteraction() {
@@ -302,9 +335,13 @@ export default class OlDrawing {
302
335
  }
303
336
  }
304
337
  prepareMenuEntriesForVertex = (_e, mapCoordinate) => {
305
- return this.hasEditableVertexAtCoordinate(mapCoordinate)
306
- ? EntryInteractionType.ENABLED
307
- : EntryInteractionType.NOT_SHOWN;
338
+ if (!this.hasEditableVertexAtCoordinate(mapCoordinate)) {
339
+ return EntryInteractionType.NOT_SHOWN;
340
+ }
341
+ if (this.isSquareOrRectangle(this.getDrawingShapeType(this.lastClosestFeature))) {
342
+ return EntryInteractionType.NOT_SHOWN;
343
+ }
344
+ return EntryInteractionType.ENABLED;
308
345
  };
309
346
  prepareMenuEntriesForShape = (_e, mapCoordinate) => {
310
347
  return this.hasEditableShapeAtCoordinate(mapCoordinate)
@@ -621,8 +658,10 @@ export default class OlDrawing {
621
658
  }
622
659
  // TODO Move as much parameters as possible into DrawingFeature
623
660
  getStyle(dFeature, olFeature) {
624
- const modifyGeometry = olFeature.get('modifyGeometry');
625
- const geometry = modifyGeometry ? modifyGeometry.geometry : olFeature.getGeometry();
661
+ const geometry = getGeometryForRendering(olFeature);
662
+ if (!geometry) {
663
+ return [];
664
+ }
626
665
  const measureFont = 'Bold ' + dFeature.measureFontSize + 'px/1 ' + dFeature.font;
627
666
  const nameFont = 'Bold ' + dFeature.nameFontSize + 'px/1 ' + dFeature.font;
628
667
  const measureColor = 'rgba(0, 0, 0, 0.4)';
@@ -782,7 +821,7 @@ export default class OlDrawing {
782
821
  const vertexStyle = dFeature.getVertexStyle();
783
822
  // Add a node style to every vertex of the geometry
784
823
  vertexStyle.setGeometry(function (f) {
785
- const geom = f?.getGeometry();
824
+ const geom = getGeometryForRendering(f);
786
825
  if (geom && geom instanceof Geometry) {
787
826
  return extractVerticesFromGeometry(geom);
788
827
  }
@@ -800,11 +839,79 @@ export default class OlDrawing {
800
839
  this.context.userInteractionManager.unregisterListener('map.select', this.toolName);
801
840
  }
802
841
  removeModifyInteraction() {
842
+ this.clearModifyFeatureChangeListeners();
803
843
  if (this.modify) {
804
844
  this.map.olMap.removeInteraction(this.modify);
805
845
  this.modify = null;
806
846
  }
807
847
  }
848
+ getDrawingShapeType(olFeature) {
849
+ if (!olFeature) {
850
+ return undefined;
851
+ }
852
+ return this.drawingState.features.find((f) => f.id === olFeature.getId())?.type;
853
+ }
854
+ isSquareOrRectangle(shapeType) {
855
+ return shapeType === DrawingShape.Square || shapeType === DrawingShape.Rectangle;
856
+ }
857
+ shouldAllowVertexInsertionAtEvent(e) {
858
+ const hoveredFeature = this.getHoveredModifiableFeatureAtPixel(e.pixel);
859
+ return shouldAllowVertexInsertionForShape(this.getDrawingShapeType(hoveredFeature));
860
+ }
861
+ getHoveredModifiableFeatureAtPixel(pixel) {
862
+ const modifiableFeatureSet = new Set(this.modifiableFeatures.getArray());
863
+ const hoveredFeature = this.map.olMap.forEachFeatureAtPixel(pixel, (feature, layer) => {
864
+ if (!(feature instanceof Feature) || layer !== this.drawingLayer || !modifiableFeatureSet.has(feature)) {
865
+ return undefined;
866
+ }
867
+ return feature;
868
+ }, {
869
+ hitTolerance: this.map.pixelTolerance
870
+ });
871
+ return hoveredFeature instanceof Feature ? hoveredFeature : null;
872
+ }
873
+ applyScaleRotateOnModifyStyle(modifyPointFeature) {
874
+ const modifyPointGeometry = modifyPointFeature.getGeometry();
875
+ if (!(modifyPointGeometry instanceof Point)) {
876
+ return;
877
+ }
878
+ const currentPoint = modifyPointGeometry.getCoordinates();
879
+ const features = modifyPointFeature.get('features');
880
+ if (!features) {
881
+ return;
882
+ }
883
+ features.forEach((olFeature) => {
884
+ const shapeType = this.getDrawingShapeType(olFeature);
885
+ if (!this.isSquareOrRectangle(shapeType)) {
886
+ return;
887
+ }
888
+ const modifyGeometry = olFeature.get('modifyGeometry');
889
+ if (!modifyGeometry?.geometry) {
890
+ return;
891
+ }
892
+ if (!modifyGeometry.point ||
893
+ !modifyGeometry.geometry0 ||
894
+ !modifyGeometry.center ||
895
+ modifyGeometry.minRadius === undefined) {
896
+ modifyGeometry.point = [...currentPoint];
897
+ modifyGeometry.geometry0 = modifyGeometry.geometry;
898
+ const geometryData = calculateCenterAndMinRadius(modifyGeometry.geometry0);
899
+ modifyGeometry.center = geometryData.center;
900
+ modifyGeometry.minRadius = geometryData.minRadius;
901
+ }
902
+ if (!modifyGeometry.geometry0 || !modifyGeometry.center || modifyGeometry.minRadius === undefined) {
903
+ return;
904
+ }
905
+ modifyGeometry.geometry = createScaledAndRotatedGeometry(modifyGeometry.geometry0, modifyGeometry.center, modifyGeometry.minRadius, modifyGeometry.point, currentPoint);
906
+ });
907
+ }
908
+ clearModifyFeatureChangeListeners() {
909
+ if (this.modifyFeatureChangeListeners.length === 0) {
910
+ return;
911
+ }
912
+ unByKeyAll(this.modifyFeatureChangeListeners);
913
+ this.modifyFeatureChangeListeners.length = 0;
914
+ }
808
915
  removeSnapInteraction() {
809
916
  if (this.snap) {
810
917
  this.map.olMap.removeInteraction(this.snap);
@@ -0,0 +1,11 @@
1
+ import { Coordinate } from 'ol/coordinate.js';
2
+ import Geometry from 'ol/geom/Geometry.js';
3
+ import Feature from 'ol/Feature.js';
4
+ import { DrawingShape } from './drawingFeature.js';
5
+ export declare function calculateCenterAndMinRadius(geometry: Geometry): {
6
+ center: Coordinate;
7
+ minRadius: number;
8
+ };
9
+ export declare function createScaledAndRotatedGeometry(geometry0: Geometry, center: Coordinate, minRadius: number, initialPoint: Coordinate, currentPoint: Coordinate): Geometry;
10
+ export declare function getGeometryForRendering(feature: Feature<Geometry>): Geometry | undefined;
11
+ export declare function shouldAllowVertexInsertionForShape(shapeType: DrawingShape | undefined): boolean;
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { getCenter, getHeight, getWidth } from 'ol/extent.js';
3
+ import LineString from 'ol/geom/LineString.js';
4
+ import Polygon from 'ol/geom/Polygon.js';
5
+ import { DrawingShape } from './drawingFeature.js';
6
+ const EPSILON = 1e-12;
7
+ function sqDistance(a, b) {
8
+ const dx = a[0] - b[0];
9
+ const dy = a[1] - b[1];
10
+ return dx * dx + dy * dy;
11
+ }
12
+ export function calculateCenterAndMinRadius(geometry) {
13
+ let center;
14
+ let coordinates;
15
+ if (geometry instanceof Polygon) {
16
+ const ring = geometry.getCoordinates()[0];
17
+ const polygonCoords = ring.slice(0, -1);
18
+ const x = polygonCoords.reduce((sum, c) => sum + c[0], 0);
19
+ const y = polygonCoords.reduce((sum, c) => sum + c[1], 0);
20
+ center = [x / polygonCoords.length, y / polygonCoords.length];
21
+ coordinates = polygonCoords;
22
+ }
23
+ else if (geometry instanceof LineString) {
24
+ center = geometry.getCoordinateAt(0.5);
25
+ coordinates = geometry.getCoordinates();
26
+ }
27
+ else {
28
+ center = getCenter(geometry.getExtent());
29
+ }
30
+ if (coordinates && coordinates.length > 0) {
31
+ const maxSqDistance = Math.max(...coordinates.map((coordinate) => sqDistance(coordinate, center)));
32
+ return {
33
+ center,
34
+ minRadius: Math.sqrt(maxSqDistance) / 3
35
+ };
36
+ }
37
+ return {
38
+ center,
39
+ minRadius: Math.max(getWidth(geometry.getExtent()), getHeight(geometry.getExtent())) / 3
40
+ };
41
+ }
42
+ export function createScaledAndRotatedGeometry(geometry0, center, minRadius, initialPoint, currentPoint) {
43
+ const initialRadius = Math.sqrt(sqDistance(initialPoint, center));
44
+ if (initialRadius <= minRadius) {
45
+ return geometry0.clone();
46
+ }
47
+ const currentRadius = Math.sqrt(sqDistance(currentPoint, center));
48
+ if (currentRadius <= EPSILON) {
49
+ return geometry0.clone();
50
+ }
51
+ const initialAngle = Math.atan2(initialPoint[1] - center[1], initialPoint[0] - center[0]);
52
+ const currentAngle = Math.atan2(currentPoint[1] - center[1], currentPoint[0] - center[0]);
53
+ const geometry = geometry0.clone();
54
+ geometry.scale(currentRadius / initialRadius, undefined, center);
55
+ geometry.rotate(currentAngle - initialAngle, center);
56
+ return geometry;
57
+ }
58
+ export function getGeometryForRendering(feature) {
59
+ const modifyGeometry = feature.get('modifyGeometry');
60
+ return modifyGeometry?.geometry ?? feature.getGeometry() ?? undefined;
61
+ }
62
+ export function shouldAllowVertexInsertionForShape(shapeType) {
63
+ return shapeType !== DrawingShape.Square && shapeType !== DrawingShape.Rectangle;
64
+ }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "GeoGirafe PSC",
6
6
  "url": "https://doc.geomapfish.dev"
7
7
  },
8
- "version": "1.1.0-dev.2456794124",
8
+ "version": "1.1.0-dev.2463680603",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -1 +1 @@
1
- {"version":"1.1.0-dev.2456794124", "build":"2456794124", "date":"16/04/2026"}
1
+ {"version":"1.1.0-dev.2463680603", "build":"2463680603", "date":"19/04/2026"}