@open-pioneer/feature-editing 1.3.0-dev-attributions.20260422081717
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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +202 -0
- package/README.md +585 -0
- package/_virtual/hooks.js +8 -0
- package/_virtual/hooks.js.map +1 -0
- package/_virtual/source-info.js +4 -0
- package/_virtual/source-info.js.map +1 -0
- package/api/editor/context.d.ts +136 -0
- package/api/editor/context.js +6 -0
- package/api/editor/context.js.map +1 -0
- package/api/editor/editor.d.ts +125 -0
- package/api/editor/editor.js +6 -0
- package/api/editor/editor.js.map +1 -0
- package/api/fields/BaseFieldConfig.d.ts +97 -0
- package/api/fields/CustomFieldConfig.d.ts +48 -0
- package/api/fields/FieldConfig.d.ts +20 -0
- package/api/fields/optionFieldConfigs.d.ts +103 -0
- package/api/fields/standardFieldConfigs.d.ts +164 -0
- package/api/model/EditingStep.d.ts +96 -0
- package/api/model/FeatureTemplate.d.ts +156 -0
- package/api/model/FeatureWriter.d.ts +133 -0
- package/api/model/InteractionOptions.d.ts +73 -0
- package/i18n/de.yaml +35 -0
- package/i18n/en.yaml +35 -0
- package/implementation/FeatureEditor.d.ts +3 -0
- package/implementation/FeatureEditor.js +105 -0
- package/implementation/FeatureEditor.js.map +1 -0
- package/implementation/components/action-selector/ActionSelector.d.ts +18 -0
- package/implementation/components/action-selector/ActionSelector.js +68 -0
- package/implementation/components/action-selector/ActionSelector.js.map +1 -0
- package/implementation/components/action-selector/DrawingControls.d.ts +6 -0
- package/implementation/components/action-selector/DrawingControls.js +79 -0
- package/implementation/components/action-selector/DrawingControls.js.map +1 -0
- package/implementation/components/action-selector/SelectButton.d.ts +7 -0
- package/implementation/components/action-selector/SelectButton.js +26 -0
- package/implementation/components/action-selector/SelectButton.js.map +1 -0
- package/implementation/components/action-selector/TemplateItem.d.ts +9 -0
- package/implementation/components/action-selector/TemplateItem.js +62 -0
- package/implementation/components/action-selector/TemplateItem.js.map +1 -0
- package/implementation/components/action-selector/TemplateSelector.d.ts +9 -0
- package/implementation/components/action-selector/TemplateSelector.js +31 -0
- package/implementation/components/action-selector/TemplateSelector.js.map +1 -0
- package/implementation/components/controls/ColorPickerControl.d.ts +9 -0
- package/implementation/components/controls/ColorPickerControl.js +40 -0
- package/implementation/components/controls/ColorPickerControl.js.map +1 -0
- package/implementation/components/controls/ComboBoxControl.d.ts +4 -0
- package/implementation/components/controls/ComboBoxControl.js +39 -0
- package/implementation/components/controls/ComboBoxControl.js.map +1 -0
- package/implementation/components/controls/DefaultControl.d.ts +6 -0
- package/implementation/components/controls/DefaultControl.js +144 -0
- package/implementation/components/controls/DefaultControl.js.map +1 -0
- package/implementation/components/controls/NumberFieldControl.d.ts +9 -0
- package/implementation/components/controls/NumberFieldControl.js +69 -0
- package/implementation/components/controls/NumberFieldControl.js.map +1 -0
- package/implementation/components/controls/RadioGroupControl.d.ts +4 -0
- package/implementation/components/controls/RadioGroupControl.js +26 -0
- package/implementation/components/controls/RadioGroupControl.js.map +1 -0
- package/implementation/components/controls/SelectControl.d.ts +4 -0
- package/implementation/components/controls/SelectControl.js +24 -0
- package/implementation/components/controls/SelectControl.js.map +1 -0
- package/implementation/components/controls/option-hooks.d.ts +21 -0
- package/implementation/components/controls/option-hooks.js +41 -0
- package/implementation/components/controls/option-hooks.js.map +1 -0
- package/implementation/components/property-editor/ButtonRow.d.ts +10 -0
- package/implementation/components/property-editor/ButtonRow.js +74 -0
- package/implementation/components/property-editor/ButtonRow.js.map +1 -0
- package/implementation/components/property-editor/DeleteConfirmationDialog.d.ts +8 -0
- package/implementation/components/property-editor/DeleteConfirmationDialog.js +58 -0
- package/implementation/components/property-editor/DeleteConfirmationDialog.js.map +1 -0
- package/implementation/components/property-editor/PropertyEditor.d.ts +5 -0
- package/implementation/components/property-editor/PropertyEditor.js +48 -0
- package/implementation/components/property-editor/PropertyEditor.js.map +1 -0
- package/implementation/components/property-editor/PropertyField.d.ts +6 -0
- package/implementation/components/property-editor/PropertyField.js +54 -0
- package/implementation/components/property-editor/PropertyField.js.map +1 -0
- package/implementation/components/property-editor/PropertyForm.d.ts +8 -0
- package/implementation/components/property-editor/PropertyForm.js +82 -0
- package/implementation/components/property-editor/PropertyForm.js.map +1 -0
- package/implementation/context/PropertyFormContext.d.ts +25 -0
- package/implementation/context/PropertyFormContext.js +52 -0
- package/implementation/context/PropertyFormContext.js.map +1 -0
- package/implementation/context/PropertyFormContextProvider.d.ts +9 -0
- package/implementation/context/PropertyFormContextProvider.js +18 -0
- package/implementation/context/PropertyFormContextProvider.js.map +1 -0
- package/implementation/context/usePropertyFormContext.d.ts +2 -0
- package/implementation/context/usePropertyFormContext.js +14 -0
- package/implementation/context/usePropertyFormContext.js.map +1 -0
- package/implementation/editor/editorHooks.d.ts +10 -0
- package/implementation/editor/editorHooks.js +56 -0
- package/implementation/editor/editorHooks.js.map +1 -0
- package/implementation/editor/useEditingCallbacks.d.ts +9 -0
- package/implementation/editor/useEditingCallbacks.js +108 -0
- package/implementation/editor/useEditingCallbacks.js.map +1 -0
- package/implementation/featureEditing.css +15 -0
- package/implementation/featureEditing.css.map +1 -0
- package/implementation/geometry-editing/controller/DrawingSession.d.ts +31 -0
- package/implementation/geometry-editing/controller/DrawingSession.js +92 -0
- package/implementation/geometry-editing/controller/DrawingSession.js.map +1 -0
- package/implementation/geometry-editing/controller/EditingController.d.ts +35 -0
- package/implementation/geometry-editing/controller/EditingController.js +94 -0
- package/implementation/geometry-editing/controller/EditingController.js.map +1 -0
- package/implementation/geometry-editing/controller/UndoManager.d.ts +12 -0
- package/implementation/geometry-editing/controller/UndoManager.js +39 -0
- package/implementation/geometry-editing/controller/UndoManager.js.map +1 -0
- package/implementation/geometry-editing/controller/geometryState.d.ts +6 -0
- package/implementation/geometry-editing/controller/geometryState.js +45 -0
- package/implementation/geometry-editing/controller/geometryState.js.map +1 -0
- package/implementation/geometry-editing/interactions/BaseInteraction.d.ts +14 -0
- package/implementation/geometry-editing/interactions/BaseInteraction.js +28 -0
- package/implementation/geometry-editing/interactions/BaseInteraction.js.map +1 -0
- package/implementation/geometry-editing/interactions/DoubleClickInteraction.d.ts +14 -0
- package/implementation/geometry-editing/interactions/DoubleClickInteraction.js +23 -0
- package/implementation/geometry-editing/interactions/DoubleClickInteraction.js.map +1 -0
- package/implementation/geometry-editing/interactions/DrawingInteraction.d.ts +31 -0
- package/implementation/geometry-editing/interactions/DrawingInteraction.js +85 -0
- package/implementation/geometry-editing/interactions/DrawingInteraction.js.map +1 -0
- package/implementation/geometry-editing/interactions/HighlightingInteraction.d.ts +17 -0
- package/implementation/geometry-editing/interactions/HighlightingInteraction.js +43 -0
- package/implementation/geometry-editing/interactions/HighlightingInteraction.js.map +1 -0
- package/implementation/geometry-editing/interactions/KeyboardInteraction.d.ts +19 -0
- package/implementation/geometry-editing/interactions/KeyboardInteraction.js +54 -0
- package/implementation/geometry-editing/interactions/KeyboardInteraction.js.map +1 -0
- package/implementation/geometry-editing/interactions/ModificationInteraction.d.ts +26 -0
- package/implementation/geometry-editing/interactions/ModificationInteraction.js +52 -0
- package/implementation/geometry-editing/interactions/ModificationInteraction.js.map +1 -0
- package/implementation/geometry-editing/interactions/SelectionInteraction.d.ts +27 -0
- package/implementation/geometry-editing/interactions/SelectionInteraction.js +58 -0
- package/implementation/geometry-editing/interactions/SelectionInteraction.js.map +1 -0
- package/implementation/geometry-editing/interactions/SnappingInteraction.d.ts +16 -0
- package/implementation/geometry-editing/interactions/SnappingInteraction.js +21 -0
- package/implementation/geometry-editing/interactions/SnappingInteraction.js.map +1 -0
- package/implementation/geometry-editing/useGeometryEditing.d.ts +145 -0
- package/implementation/geometry-editing/useGeometryEditing.js +79 -0
- package/implementation/geometry-editing/useGeometryEditing.js.map +1 -0
- package/index.d.ts +26 -0
- package/index.js +3 -0
- package/index.js.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
# @open-pioneer/feature-editing
|
|
2
|
+
|
|
3
|
+
This package provides editing functionality for map features.
|
|
4
|
+
It supports creating, modifying, and deleting features with declarative form configuration and custom UI components.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Feature creation**: Draw new geometries (points, lines, polygons) on the map
|
|
9
|
+
- **Feature modification**: Edit existing feature geometries and attributes
|
|
10
|
+
- **Feature deletion**: Remove features with confirmation dialog
|
|
11
|
+
- **Declarative forms**: Configure property forms using simple field configurations
|
|
12
|
+
- **Custom forms**: Build completely custom forms for advanced use cases
|
|
13
|
+
- **Dynamic field behavior**: Conditional visibility, validation, and enabled states
|
|
14
|
+
- **Snapping**: Snap to existing features during drawing and modification
|
|
15
|
+
- **Undo/redo**: Built-in support for geometry editing history
|
|
16
|
+
|
|
17
|
+
The Editing works with all vector layer sources and can therefore be used with any layer type that supports feature selection, such as `GeoJSON`, `WFS`, or `OGC API Features`.
|
|
18
|
+
As the package does not manage feature storage or how changes are applied to the map, it fits a wide range of data management strategies and provides full flexibility to the using app.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
The simplest way to use the FeatureEditor is to provide feature templates and a `writer` implementation:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import {
|
|
26
|
+
FeatureEditor,
|
|
27
|
+
type FeatureWriter,
|
|
28
|
+
type FeatureTemplate
|
|
29
|
+
} from "@open-pioneer/feature-editing";
|
|
30
|
+
|
|
31
|
+
function EditorComponent() {
|
|
32
|
+
return <FeatureEditor templates={templates} writer={featureWriter} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const templates: FeatureTemplate[] = [
|
|
36
|
+
{
|
|
37
|
+
name: "Point of Interest",
|
|
38
|
+
kind: "declarative",
|
|
39
|
+
geometryType: "Point",
|
|
40
|
+
layerId: "poi-layer", // creation not shown in this example
|
|
41
|
+
fields: [
|
|
42
|
+
{ label: "Name", type: "text-field", propertyName: "name", isRequired: true },
|
|
43
|
+
{ label: "Description", type: "text-area", propertyName: "description" }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const featureWriter: FeatureWriter = {
|
|
49
|
+
async addFeature(feature, template, projection) {
|
|
50
|
+
// Persist the new feature in your backend, then update the map
|
|
51
|
+
await myApi.createFeature(feature, template.layerId);
|
|
52
|
+
},
|
|
53
|
+
async updateFeature({ feature, layer, projection }) {
|
|
54
|
+
// Update the existing feature in your backend, then update the map
|
|
55
|
+
await myApi.updateFeature(feature, layer?.id);
|
|
56
|
+
},
|
|
57
|
+
async deleteFeature({ feature, layer, projection }) {
|
|
58
|
+
// Delete the feature from your backend, then update the map
|
|
59
|
+
await myApi.deleteFeature(feature, layer?.id);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Core Concepts
|
|
65
|
+
|
|
66
|
+
### Feature Templates
|
|
67
|
+
|
|
68
|
+
Feature templates define what types of features can be created and how their properties are edited. Each template specifies:
|
|
69
|
+
|
|
70
|
+
- **Geometry type**: `"Point"`, `"LineString"`, `"Polygon"` or `"Circle"`. It is also possible to create _rectangle_ geometries (see example below).
|
|
71
|
+
- **Form configuration**: Either declarative fields or a custom render function
|
|
72
|
+
- **Layer association**: Links the template to a map layer for selection
|
|
73
|
+
- **Drawing options**: Customizes the OpenLayers Draw interaction
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
const template: FeatureTemplate = {
|
|
77
|
+
// Display name shown in the UI
|
|
78
|
+
name: "Forest Damage Report",
|
|
79
|
+
|
|
80
|
+
// Form configuration (declarative or dynamic)
|
|
81
|
+
kind: "declarative",
|
|
82
|
+
|
|
83
|
+
// Optional icon displayed in template selector
|
|
84
|
+
icon: <TreeIcon />,
|
|
85
|
+
|
|
86
|
+
// Geometry type for new features
|
|
87
|
+
geometryType: "Point",
|
|
88
|
+
|
|
89
|
+
// Associated layer ID for feature selection
|
|
90
|
+
layerId: "forest-damage",
|
|
91
|
+
|
|
92
|
+
// Default property values for new features
|
|
93
|
+
defaultProperties: {
|
|
94
|
+
status: "pending",
|
|
95
|
+
reportedAt: new Date().toISOString()
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Form fields (for declarative templates)
|
|
99
|
+
fields: [
|
|
100
|
+
{ label: "Tree Species", type: "text-field", propertyName: "species" }
|
|
101
|
+
// ... more fields
|
|
102
|
+
],
|
|
103
|
+
|
|
104
|
+
// Optional drawing customization
|
|
105
|
+
drawingOptions: {
|
|
106
|
+
style: new Style({
|
|
107
|
+
/* ... */
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Example: Rectangle geometries
|
|
114
|
+
|
|
115
|
+
To create rectangle geometries, set `geometryType` to `"Circle"` and provide a `geometryFunction` that creates rectangle geometries using the `createBox` function from OpenLayers:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { createBox } from "ol/interaction/Draw";
|
|
119
|
+
|
|
120
|
+
const template: FeatureTemplate = {
|
|
121
|
+
kind: "declarative",
|
|
122
|
+
geometryType: "Circle",
|
|
123
|
+
|
|
124
|
+
// ...
|
|
125
|
+
|
|
126
|
+
drawingOptions: {
|
|
127
|
+
geometryFunction: createBox()
|
|
128
|
+
},
|
|
129
|
+
fields: [
|
|
130
|
+
// ...
|
|
131
|
+
]
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Data flow
|
|
136
|
+
|
|
137
|
+
The editor supports editing _existing_ features or creating _new_ features:
|
|
138
|
+
|
|
139
|
+
- Existing features are taken from the map, using a workflow based on OpenLayer's [Select](https://openlayers.org/en/latest/apidoc/module-ol_interaction_Select-Select.html) interaction.
|
|
140
|
+
The feature object and its attributes are taken directly from the layer (typically some kind of `VectorLayer`).
|
|
141
|
+
- New feature objects are created based on the configured feature template (i.e. `defaultProperties`, `geometryType`, etc.).
|
|
142
|
+
The user can draw the feature's geometry on a temporary layer.
|
|
143
|
+
|
|
144
|
+
In both cases, feature attributes can be edited using the editor's form controls.
|
|
145
|
+
Note however, that the layers or their sources in the map are **never** modified directly.
|
|
146
|
+
Instead, the editor calls the `FeatureWriter`'s methods to apply any changes made by the user (see [FeatureWriter](#featurewriter) below).
|
|
147
|
+
|
|
148
|
+
In summary, the sequence of steps when creating or editing a feature is as follows:
|
|
149
|
+
|
|
150
|
+
1. A feature is either selected from the map or created based on the feature template.
|
|
151
|
+
2. The user edits the feature's geometry or properties using the editor component.
|
|
152
|
+
3. The editor calls `addFeature`, `updateFeature` or `deleteFeature` on the `featureWriter` based on the user's actions.
|
|
153
|
+
The feature writer ensures that:
|
|
154
|
+
- Changes are persisted (if needed)
|
|
155
|
+
- _Vector sources are updated_, i.e. changes are applied to the map
|
|
156
|
+
4. Optional: if the user selects a feature again, they will observe the updated attributes / geometries.
|
|
157
|
+
|
|
158
|
+
#### Example
|
|
159
|
+
|
|
160
|
+
The following self-contained example demonstrates of all the relevant bits and pieces.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { FeatureEditor, FeatureWriter, type FeatureTemplate } from "@open-pioneer/feature-editing";
|
|
164
|
+
import { LayerFactory, MapModel, SimpleLayer, useMapModelValue } from "@open-pioneer/map";
|
|
165
|
+
import { SectionHeading, TitledSection } from "@open-pioneer/react-utils";
|
|
166
|
+
import { Collection, Feature } from "ol";
|
|
167
|
+
import VectorLayer from "ol/layer/Vector";
|
|
168
|
+
import VectorSource from "ol/source/Vector";
|
|
169
|
+
import { useService } from "open-pioneer:react-hooks";
|
|
170
|
+
import { useEffect, useMemo } from "react";
|
|
171
|
+
|
|
172
|
+
export function SimpleEditorComponent() {
|
|
173
|
+
const map = useMapModelValue();
|
|
174
|
+
const { featureWriter, template } = useEditingSetup(map);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<TitledSection>
|
|
178
|
+
<SectionHeading size="md" mb={2}>
|
|
179
|
+
Editor
|
|
180
|
+
</SectionHeading>
|
|
181
|
+
<FeatureEditor templates={[template]} writer={featureWriter} />
|
|
182
|
+
</TitledSection>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Extremely basic editing example to demonstrate the different bits and their interactions.
|
|
187
|
+
// In a real application, you would typically:
|
|
188
|
+
// - have more than one layer (maybe even more than one feature template per layer)
|
|
189
|
+
// - have actual persistence
|
|
190
|
+
// - split the bits into multiple classes and files in more sensible locations
|
|
191
|
+
// - ...
|
|
192
|
+
function useEditingSetup(map: MapModel) {
|
|
193
|
+
const layerFactory = useService<LayerFactory>("map.LayerFactory");
|
|
194
|
+
const options = useMemo(() => {
|
|
195
|
+
// The collection contains the layer's features.
|
|
196
|
+
// The collection is shared by the layer (read side, OpenLayers support) and the FeatureWriter implementation (write side).
|
|
197
|
+
// This is the simplest way to notify the OpenLayers VectorLayer about changes in the underlying data.
|
|
198
|
+
const featureCollection = new Collection<Feature>();
|
|
199
|
+
|
|
200
|
+
// Callback to "store" data (just a fancy array in this case).
|
|
201
|
+
const featureWriter: FeatureWriter = {
|
|
202
|
+
addFeature: async ({ feature }) => {
|
|
203
|
+
featureCollection.push(feature);
|
|
204
|
+
},
|
|
205
|
+
updateFeature: async ({ feature }) => {
|
|
206
|
+
const index = featureCollection.getArray().indexOf(feature);
|
|
207
|
+
if (index >= 0) {
|
|
208
|
+
// Triggers an update, even though we write the same feature again.
|
|
209
|
+
featureCollection.setAt(index, feature);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
deleteFeature: async ({ feature }) => {
|
|
213
|
+
featureCollection.remove(feature);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// The template defines the form content when a feature is being edited / created.
|
|
218
|
+
// It also contains the defaultAttributes and the geometryType for _new_ features.
|
|
219
|
+
const template: FeatureTemplate = {
|
|
220
|
+
name: "Simple Point Feature",
|
|
221
|
+
kind: "declarative",
|
|
222
|
+
geometryType: "Point",
|
|
223
|
+
layerId: "simple-editable-layer",
|
|
224
|
+
fields: [
|
|
225
|
+
{ label: "Title", type: "text-field", propertyName: "title", isRequired: false }
|
|
226
|
+
],
|
|
227
|
+
defaultProperties: {
|
|
228
|
+
title: ""
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
return { featureCollection, featureWriter, template };
|
|
232
|
+
}, []);
|
|
233
|
+
|
|
234
|
+
// Quick and dirty: mount layer in the map
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
// Layer for visualization and selection in the map.
|
|
237
|
+
const layer = layerFactory.create({
|
|
238
|
+
type: SimpleLayer,
|
|
239
|
+
id: "simple-editable-layer",
|
|
240
|
+
title: "Simple Editable Layer",
|
|
241
|
+
olLayer: new VectorLayer({
|
|
242
|
+
source: new VectorSource({
|
|
243
|
+
features: options.featureCollection
|
|
244
|
+
}),
|
|
245
|
+
updateWhileAnimating: true,
|
|
246
|
+
updateWhileInteracting: true
|
|
247
|
+
})
|
|
248
|
+
});
|
|
249
|
+
map.layers.addLayer(layer);
|
|
250
|
+
return () => {
|
|
251
|
+
map.layers.removeLayer(layer);
|
|
252
|
+
layer.destroy();
|
|
253
|
+
};
|
|
254
|
+
}, [map, layerFactory, options.featureCollection]);
|
|
255
|
+
return options;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### FeatureWriter
|
|
260
|
+
|
|
261
|
+
The editor does not manage the lifetime of any features in the map, except for the feature currently being edited.
|
|
262
|
+
To persist changes made by the editor, you must implement the `FeatureWriter` interface.
|
|
263
|
+
|
|
264
|
+
The interface is flexible enough to work in many scenarios:
|
|
265
|
+
|
|
266
|
+
- Transient client side state (e.g. an in-memory collection used for a `VectorSource`)
|
|
267
|
+
- Persistent client side storage (e.g. Origin private file system)
|
|
268
|
+
- Service side storage (e.g. REST APIs)
|
|
269
|
+
|
|
270
|
+
All functions are async and should apply the changes made by the user to the backing data structures or services.
|
|
271
|
+
For example, to persist changes using a (fictional) REST API:
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
const featureWriter: FeatureWriter = {
|
|
275
|
+
// Called when a new feature is created
|
|
276
|
+
async addFeature({ feature, template, projection }) {
|
|
277
|
+
const geojson = new GeoJSON().writeFeatureObject(feature, {
|
|
278
|
+
featureProjection: projection
|
|
279
|
+
});
|
|
280
|
+
await fetch(`/api/layers/${template.layerId}/features`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
body: JSON.stringify(geojson)
|
|
283
|
+
});
|
|
284
|
+
// Update map/layers (e.g. VectorSources)
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
// Called when an existing feature is modified
|
|
288
|
+
async updateFeature({ feature, layer, projection }) {
|
|
289
|
+
const id = feature.getId();
|
|
290
|
+
const geojson = new GeoJSON().writeFeatureObject(feature, {
|
|
291
|
+
featureProjection: projection
|
|
292
|
+
});
|
|
293
|
+
await fetch(`/api/layers/${layer?.id}/features/${id}`, {
|
|
294
|
+
method: "PUT",
|
|
295
|
+
body: JSON.stringify(geojson)
|
|
296
|
+
});
|
|
297
|
+
// Update map/layers (e.g. VectorSources)
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// Called when a feature is deleted
|
|
301
|
+
async deleteFeature({ feature, layer, projection }) {
|
|
302
|
+
const id = feature.getId();
|
|
303
|
+
await fetch(`/api/layers/${layer?.id}/features/${id}`, {
|
|
304
|
+
method: "DELETE"
|
|
305
|
+
});
|
|
306
|
+
// Update map/layers (e.g. VectorSources)
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
If a storage function throws an error, it is logged and a generic error notification is shown to the user.
|
|
312
|
+
On success, a success notification is shown.
|
|
313
|
+
|
|
314
|
+
To supply an additional message for the error notification, return an object of type `StorageError` instead:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const featureWriter: FeatureWriter = {
|
|
318
|
+
addFeature: async ({ feature, template, projection }) => {
|
|
319
|
+
// ...
|
|
320
|
+
LOG.error("Failed to do X", error);
|
|
321
|
+
return { kind: "error", message: "Insufficient permissions" };
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## FeatureEditor Props
|
|
327
|
+
|
|
328
|
+
The `FeatureEditor` component accepts the following props:
|
|
329
|
+
|
|
330
|
+
| Prop | Type | Required | Description |
|
|
331
|
+
| -------------------------------- | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
332
|
+
| `map` | `MapModel` | No | Map model to use (defaults to context map) |
|
|
333
|
+
| `templates` | `FeatureTemplate[]` | Yes | Feature templates defining the types of features that can be created or edited |
|
|
334
|
+
| `writer` | `FeatureWriter` | Yes | Storage implementation for feature create, update, and delete operations |
|
|
335
|
+
| `selectableLayers` | `Layer[]` | No | Layers from which features can be selected. Defaults to layers matching template layer IDs |
|
|
336
|
+
| `snappableLayers` | `Layer[]` | No | Layers for snapping during drawing/modification. Defaults to `selectableLayers` |
|
|
337
|
+
| `resolveFormTemplate` | `(context) => FormTemplate \| undefined` | No | Custom function to determine which form template to use when editing an existing feature (see below) |
|
|
338
|
+
| `showActionBar` | `boolean` | No | Whether to show undo/redo/finish/reset controls during drawing (default: `true`) |
|
|
339
|
+
| `successNotifierDisplayDuration` | `number \| false` | No | Duration in ms to display success notifications. By default, never disappears. Use `false` to completely hide the notification. |
|
|
340
|
+
| `failureNotifierDisplayDuration` | `number \| false` | No | Duration in ms to display failure notifications. By default, never disappears. Use `false` to completely hide the notification. |
|
|
341
|
+
| `onEditingStepChange` | `(newEditingStep) => void` | No | Callback invoked when the editing workflow step changes |
|
|
342
|
+
|
|
343
|
+
### Interaction Options
|
|
344
|
+
|
|
345
|
+
The FeatureEditor also accepts OpenLayers interaction options for fine-grained control:
|
|
346
|
+
|
|
347
|
+
| Prop | Type | Description |
|
|
348
|
+
| --------------------- | --------------------- | ---------------------------------- |
|
|
349
|
+
| `drawingOptions` | `DrawingOptions` | Options for the Draw interaction |
|
|
350
|
+
| `selectionOptions` | `SelectionOptions` | Options for the Select interaction |
|
|
351
|
+
| `modificationOptions` | `ModificationOptions` | Options for the Modify interaction |
|
|
352
|
+
| `snappingOptions` | `SnappingOptions` | Options for the Snap interaction |
|
|
353
|
+
| `highlightingOptions` | `HighlightOptions` | Options for feature highlighting |
|
|
354
|
+
|
|
355
|
+
The drawing options will be merged with those of the selected feature template (if any are given), with the ones of the template taking precedence.
|
|
356
|
+
|
|
357
|
+
### Resolving Form Templates
|
|
358
|
+
|
|
359
|
+
When the user selects an existing feature on the map, the FeatureEditor needs to determine which form template to use.
|
|
360
|
+
By default, it matches the feature's layer ID against the `layerId` of each template in the `templates` array and uses the first match.
|
|
361
|
+
|
|
362
|
+
For more control — such as when features from the same layer require different forms based on their attributes — you can provide a custom `resolveFormTemplate` callback:
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
const resolveFormTemplate = ({ feature, layer }: FormTemplateContext) => {
|
|
366
|
+
const type = feature.get("type");
|
|
367
|
+
return type === "residential" ? residentialTemplate : commercialTemplate;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
<FeatureEditor
|
|
371
|
+
templates={[residentialTemplate, commercialTemplate]}
|
|
372
|
+
resolveFormTemplate={resolveFormTemplate}
|
|
373
|
+
writer={featureWriter}
|
|
374
|
+
/>;
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Field Types
|
|
378
|
+
|
|
379
|
+
### Standard Fields
|
|
380
|
+
|
|
381
|
+
| Type | Description | Key Properties |
|
|
382
|
+
| -------------- | -------------------------------------- | --------------------------------------------------------- |
|
|
383
|
+
| `text-field` | Single-line text input | `placeholder` |
|
|
384
|
+
| `text-area` | Multi-line text input | `placeholder` |
|
|
385
|
+
| `number-field` | Numeric input with validation | `min`, `max`, `formatOptions`, `step`, `showSteppers` |
|
|
386
|
+
| `check-box` | Boolean checkbox | `checkBoxLabel` |
|
|
387
|
+
| `switch` | Boolean toggle switch | `switchLabel` |
|
|
388
|
+
| `date-picker` | Date or datetime picker | `includeTime` |
|
|
389
|
+
| `color-picker` | Color selection with optional swatches | `swatchColors` |
|
|
390
|
+
| `select` | Dropdown selection | `options`, `valueType`, `placeholder`, `showClearTrigger` |
|
|
391
|
+
| `combo-box` | Searchable dropdown | `options`, `valueType`, `placeholder`, `showClearTrigger` |
|
|
392
|
+
| `radio-group` | Radio button group | `options`, `valueType` |
|
|
393
|
+
| `custom` | Custom render function | `render` |
|
|
394
|
+
|
|
395
|
+
### Common Field Properties
|
|
396
|
+
|
|
397
|
+
All fields share these base properties:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
interface BaseFieldConfig {
|
|
401
|
+
readonly label: string; // Display label
|
|
402
|
+
readonly type: string; // Type of input control
|
|
403
|
+
readonly propertyName: string; // Feature property to edit
|
|
404
|
+
readonly isRequired?: PropertyFunctionOr<boolean>; // Required validation
|
|
405
|
+
readonly isEnabled?: PropertyFunctionOr<boolean>; // Enable/disable state
|
|
406
|
+
readonly isVisible?: PropertyFunctionOr<boolean>; // Show/hide field
|
|
407
|
+
readonly isValid?: PropertyFunctionOr<boolean>; // Custom validation
|
|
408
|
+
readonly errorText?: PropertyFunctionOr<string | undefined>; // Error message
|
|
409
|
+
readonly helperText?: PropertyFunctionOr<string | undefined>; // Info text
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Property Functions
|
|
414
|
+
|
|
415
|
+
Field properties that accept `PropertyFunctionOr<T>` can be either static values or functions that compute values dynamically based on current feature properties. This enables conditional field behavior:
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
const fields: FieldConfig[] = [
|
|
419
|
+
{
|
|
420
|
+
label: "Damage Type",
|
|
421
|
+
type: "select",
|
|
422
|
+
propertyName: "damageType",
|
|
423
|
+
valueType: "string",
|
|
424
|
+
options: [
|
|
425
|
+
{ label: "Storm", value: "storm" },
|
|
426
|
+
{ label: "Fire", value: "fire" },
|
|
427
|
+
{ label: "Pest", value: "pest" }
|
|
428
|
+
]
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
label: "Pest Species",
|
|
432
|
+
type: "text-field",
|
|
433
|
+
propertyName: "pestName",
|
|
434
|
+
// Only visible when damage type is "pest"
|
|
435
|
+
isVisible: (properties) => properties.damageType === "pest",
|
|
436
|
+
// Required only when visible
|
|
437
|
+
isRequired: (properties) => properties.damageType === "pest"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
label: "Affected Area (m²)",
|
|
441
|
+
type: "number-field",
|
|
442
|
+
propertyName: "affectedArea",
|
|
443
|
+
min: 0,
|
|
444
|
+
// Custom validation with dynamic error message
|
|
445
|
+
isValid: (properties) => {
|
|
446
|
+
const area = properties.affectedArea as number;
|
|
447
|
+
return area == null || area <= 10000;
|
|
448
|
+
},
|
|
449
|
+
errorText: "Area must not exceed 10,000 m²"
|
|
450
|
+
}
|
|
451
|
+
];
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Customization
|
|
455
|
+
|
|
456
|
+
### Custom Input Controls
|
|
457
|
+
|
|
458
|
+
Use the `custom` field type to render any input control within a declarative form. The render function receives the current value and an onChange callback:
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
import { Slider } from "@chakra-ui/react";
|
|
462
|
+
|
|
463
|
+
const template: FeatureTemplate = {
|
|
464
|
+
name: "Survey Point",
|
|
465
|
+
kind: "declarative",
|
|
466
|
+
geometryType: "Point",
|
|
467
|
+
fields: [
|
|
468
|
+
{
|
|
469
|
+
label: "Confidence Level",
|
|
470
|
+
type: "custom",
|
|
471
|
+
propertyName: "confidence",
|
|
472
|
+
render: (value, onChange) => (
|
|
473
|
+
<Slider.Root
|
|
474
|
+
value={[(value as number | undefined) ?? 50]}
|
|
475
|
+
onValueChange={(details) => onChange(details.value[0])}
|
|
476
|
+
min={0}
|
|
477
|
+
max={100}
|
|
478
|
+
>
|
|
479
|
+
<Slider.Control>
|
|
480
|
+
<Slider.Track>
|
|
481
|
+
<Slider.Range />
|
|
482
|
+
</Slider.Track>
|
|
483
|
+
<Slider.Thumb index={0} />
|
|
484
|
+
</Slider.Control>
|
|
485
|
+
</Slider.Root>
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
]
|
|
489
|
+
};
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Custom Forms with Property Form Context
|
|
493
|
+
|
|
494
|
+
For complete control over form layout and behavior, use a dynamic form template with the `usePropertyFormContext` hook:
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
import { usePropertyFormContext, type FeatureTemplate } from "@open-pioneer/feature-editing";
|
|
498
|
+
import { useReactiveSnapshot } from "@open-pioneer/reactivity";
|
|
499
|
+
import { Field, Input, VStack } from "@chakra-ui/react";
|
|
500
|
+
import { useEffect } from "react";
|
|
501
|
+
|
|
502
|
+
function CustomSurveyForm() {
|
|
503
|
+
const context = usePropertyFormContext();
|
|
504
|
+
|
|
505
|
+
// Read properties reactively - component re-renders when values change
|
|
506
|
+
const name = useReactiveSnapshot(() => context.properties.get("name") ?? "", [context]);
|
|
507
|
+
const email = useReactiveSnapshot(() => context.properties.get("email") ?? "", [context]);
|
|
508
|
+
|
|
509
|
+
// Manage form validity
|
|
510
|
+
useEffect(() => {
|
|
511
|
+
context.isValid = name.length >= 1 && email.includes("@");
|
|
512
|
+
}, [context, name, email]);
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
<VStack gap={4} align="stretch">
|
|
516
|
+
<Field.Root required>
|
|
517
|
+
<Field.Label>Surveyor Name</Field.Label>
|
|
518
|
+
<Input
|
|
519
|
+
value={name}
|
|
520
|
+
onChange={(e) => context.properties.set("name", e.target.value)}
|
|
521
|
+
/>
|
|
522
|
+
</Field.Root>
|
|
523
|
+
|
|
524
|
+
<Field.Root required>
|
|
525
|
+
<Field.Label>Email</Field.Label>
|
|
526
|
+
<Input
|
|
527
|
+
type="email"
|
|
528
|
+
value={email}
|
|
529
|
+
onChange={(e) => context.properties.set("email", e.target.value)}
|
|
530
|
+
/>
|
|
531
|
+
</Field.Root>
|
|
532
|
+
|
|
533
|
+
{/* Access editing mode */}
|
|
534
|
+
{context.mode === "update" && <p>Editing feature from layer: {context.layer?.title}</p>}
|
|
535
|
+
</VStack>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const template: FeatureTemplate = {
|
|
540
|
+
name: "Survey",
|
|
541
|
+
kind: "dynamic",
|
|
542
|
+
geometryType: "Point",
|
|
543
|
+
layerId: "surveys",
|
|
544
|
+
renderForm: () => <CustomSurveyForm />
|
|
545
|
+
};
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
The `PropertyFormContext` provides:
|
|
549
|
+
|
|
550
|
+
| Property | Type | Description |
|
|
551
|
+
| ------------------ | ------------------------------- | ------------------------------------ |
|
|
552
|
+
| `feature` | `Feature` | The OpenLayers feature being edited |
|
|
553
|
+
| `properties` | `ReactiveMap<string, unknown>` | Reactive map of feature properties |
|
|
554
|
+
| `propertiesObject` | `() => Record<string, unknown>` | Returns properties as a plain object |
|
|
555
|
+
| `mode` | `"create" \| "update"` | Current editing mode |
|
|
556
|
+
| `template` | `FeatureTemplate \| undefined` | Template used (only in create mode) |
|
|
557
|
+
| `layer` | `Layer \| undefined` | Source layer (only in update mode) |
|
|
558
|
+
| `isValid` | `boolean` | Controls save button enabled state |
|
|
559
|
+
|
|
560
|
+
## Editing Workflow Steps
|
|
561
|
+
|
|
562
|
+
The editing workflow progresses through these steps:
|
|
563
|
+
|
|
564
|
+
| Step ID | Description | Active Interaction |
|
|
565
|
+
| --------------- | ------------------------------------- | ---------------------- |
|
|
566
|
+
| `none` | No active editing operation | None |
|
|
567
|
+
| `create-draw` | Drawing a new feature's geometry | Draw |
|
|
568
|
+
| `create-modify` | Modifying a newly drawn feature | Modify + property form |
|
|
569
|
+
| `update-select` | Selecting an existing feature to edit | Select |
|
|
570
|
+
| `update-modify` | Modifying an existing feature | Modify + property form |
|
|
571
|
+
|
|
572
|
+
## Supported keyboard shortcuts (create feature)
|
|
573
|
+
|
|
574
|
+
The following keyboard shortcuts are supported while drawing the initial feature geometry (not during editing of existing features):
|
|
575
|
+
|
|
576
|
+
| Shortcut | Description |
|
|
577
|
+
| ------------------ | -------------------------------- |
|
|
578
|
+
| `Ctrl/Command + Z` | Undo last geometry change |
|
|
579
|
+
| `Ctrl/Command + Y` | Redo last undone geometry change |
|
|
580
|
+
| `Enter` | Finish drawing or modification |
|
|
581
|
+
| `Escape` | Cancel drawing or modification |
|
|
582
|
+
|
|
583
|
+
## License
|
|
584
|
+
|
|
585
|
+
Apache-2.0 (see `LICENSE` file)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useIntlInternal, useServiceInternal } from '@open-pioneer/runtime/react-integration';
|
|
2
|
+
|
|
3
|
+
const PACKAGE_NAME = "@open-pioneer/feature-editing";
|
|
4
|
+
const useService = /*@__PURE__*/ useServiceInternal.bind(undefined, PACKAGE_NAME);
|
|
5
|
+
const useIntl = /*@__PURE__*/ useIntlInternal.bind(undefined, PACKAGE_NAME);
|
|
6
|
+
|
|
7
|
+
export { useIntl, useService };
|
|
8
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-info.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
|