Skip to content

Commit

Permalink
Implemented update feature workflow in EditingService (#41) (#289)
Browse files Browse the repository at this point in the history
Co-authored-by: Antonia van Eek <a.vaneek@conterra.de>
Co-authored-by: Matheisen, Alexander <alexander.matheisen@it.nrw.de>
  • Loading branch information
3 people committed Mar 22, 2024
1 parent 8e764ce commit 434fd3e
Show file tree
Hide file tree
Showing 23 changed files with 1,986 additions and 477 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-windows-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-pioneer/editing": minor
---

Implemented update feature workflow in EditingService.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import { EventEmitter, ManualPromise, createManualPromise } from "@open-pioneer/core";
import { EventEmitter, ManualPromise, createLogger, createManualPromise } from "@open-pioneer/core";
import { MapModel, TOPMOST_LAYER_Z } from "@open-pioneer/map";
import { Draw } from "ol/interaction";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { HttpService } from "@open-pioneer/http";
import { PackageIntl } from "@open-pioneer/runtime";
import { FlatStyleLike } from "ol/style/flat";
import { FlatStyle } from "ol/style/flat";
import GeoJSON from "ol/format/GeoJSON";
import GeoJSONGeometry from "ol/format/GeoJSON";
import GeoJSONGeometryCollection from "ol/format/GeoJSON";
Expand All @@ -17,45 +17,50 @@ import Overlay from "ol/Overlay";
import { Resource } from "@open-pioneer/core";
import { unByKey } from "ol/Observable";
import { EventsKey } from "ol/events";
import { EditingWorkflowEvents, EditingWorkflowState, EditingWorkflow } from "./api";
import {
EditingWorkflowEvents,
EditingWorkflowState,
EditingWorkflow,
EditingWorkflowProps
} from "./api";
import Feature from "ol/Feature";
import { createStyles } from "./style-utils";

const LOG = createLogger("editing:EditingCreateWorkflowImpl");

// Represents a tooltip rendered on the OpenLayers map
interface Tooltip extends Resource {
overlay: Overlay;
element: HTMLDivElement;
}

interface EditingWorkflowProps {
map: MapModel;
ogcApiFeatureLayerUrl: URL;
polygonDrawStyle: FlatStyleLike;
httpService: HttpService;
intl: PackageIntl;
}

export class EditingWorkflowImpl
export class EditingCreateWorkflowImpl
extends EventEmitter<EditingWorkflowEvents>
implements EditingWorkflow
{
#waiter: ManualPromise<string | undefined> | undefined;
#waiter: ManualPromise<Record<string, string> | undefined> | undefined;

private _httpService: HttpService;
private _intl: PackageIntl;

private _map: MapModel;
private _polygonDrawStyle: FlatStyleLike;
private _polygonStyle: FlatStyle;
private _vertexStyle: FlatStyle;
private _state: EditingWorkflowState;
private _editLayerURL: URL;
private _featureId: string | undefined;

private _drawSource: VectorSource;
private _drawLayer: VectorLayer<VectorSource>;
private _editingSource: VectorSource;
private _editingLayer: VectorLayer<VectorSource>;
private _drawInteraction: Draw;
private _olMap: OlMap;
private _mapContainer: HTMLElement | undefined;
private _tooltip: Tooltip;
private _enterHandler: (e: KeyboardEvent) => void;
private _escapeHandler: (e: KeyboardEvent) => void;

private _error: Error | undefined;

private _interactionListener: Array<EventsKey>;
private _mapListener: Array<Resource>;

Expand All @@ -64,32 +69,39 @@ export class EditingWorkflowImpl
this._httpService = options.httpService;
this._intl = options.intl;

this._polygonDrawStyle = options.polygonDrawStyle;
this._polygonStyle = options.polygonStyle;
this._vertexStyle = options.vertexStyle;

this._map = options.map;
this._olMap = options.map.olMap;
this._state = "active:initialized";
this._editLayerURL = options.ogcApiFeatureLayerUrl;

this._drawSource = new VectorSource();
this._drawLayer = new VectorLayer({
source: this._drawSource,
this._editingSource = new VectorSource();
this._editingLayer = new VectorLayer({
source: this._editingSource,
zIndex: TOPMOST_LAYER_Z,
properties: {
name: "editing-layer"
}
});

this._drawInteraction = new Draw({
source: this._drawSource,
source: this._editingSource,
type: "Polygon",
style: this._polygonDrawStyle
style: createStyles({
polygon: this._polygonStyle,
vertex: this._vertexStyle
})
});

this._tooltip = this._createTooltip(this._olMap);

this._enterHandler = (e: KeyboardEvent) => {
if (e.code === "Enter" && e.target === this._olMap.getTargetElement()) {
if (
(e.code === "Enter" || e.code === "NumpadEnter") &&
e.target === this._olMap.getTargetElement()
) {
const features = this._drawInteraction.getOverlay().getSource().getFeatures();

/**
Expand All @@ -99,7 +111,7 @@ export class EditingWorkflowImpl
* "length > 4" instead of "length >= 4"
*/
if (features[0] && features[0].getGeometry().getCoordinates()[0].length > 4) {
this._drawInteraction.finishDrawing();
this.triggerSave();
}
}
};
Expand Down Expand Up @@ -129,8 +141,44 @@ export class EditingWorkflowImpl
this.emit(state);
}

private _save(feature: Feature) {
this._setState("active:saving");

const layerUrl = this._editLayerURL;

const geometry = feature.getGeometry();
if (!geometry) {
this._destroy();
this._error = new Error("no geometry available");
this.#waiter?.reject(this._error);
return;
}
const projection = this._olMap.getView().getProjection();
const geoJson = new GeoJSON({
dataProjection: projection
});
const geoJSONGeometry: GeoJSONGeometry | GeoJSONGeometryCollection =
geoJson.writeGeometryObject(geometry, {
rightHanded: true,
decimals: 10
});

saveCreatedFeature(this._httpService, layerUrl, geoJSONGeometry, projection)
.then((featureId) => {
this._featureId = featureId;
this._destroy();
this.#waiter?.resolve({ featureId: this._featureId });
})
.catch((err: Error) => {
LOG.error(err);
this._destroy();
this._error = new Error("Failed to save feature", { cause: err });
this.#waiter?.reject(this._error);
});
}

private _start() {
this._olMap.addLayer(this._drawLayer);
this._olMap.addLayer(this._editingLayer);
this._olMap.addInteraction(this._drawInteraction);

// Add EventListener on focused map to abort actual interaction via `Escape`
Expand All @@ -145,40 +193,19 @@ export class EditingWorkflowImpl
const drawStart = this._drawInteraction.on("drawstart", () => {
this._setState("active:drawing");
this._tooltip.element.textContent = this._intl.formatMessage({
id: "tooltip.continue"
id: "create.tooltip.continue"
});
});

const drawEnd = this._drawInteraction.on("drawend", (e) => {
this._setState("active:saving");

const layerUrl = this._editLayerURL;

const geometry = e.feature.getGeometry();
if (!geometry) {
const feature = e.feature;
if (!feature) {
this._destroy();
this.#waiter?.reject(new Error("no geometry available"));
this._error = new Error("no feature available");
this.#waiter?.reject(this._error);
return;
}
const projection = this._olMap.getView().getProjection();
const geoJson = new GeoJSON({
dataProjection: projection
});
const geoJSONGeometry: GeoJSONGeometry | GeoJSONGeometryCollection =
geoJson.writeGeometryObject(geometry, {
rightHanded: true,
decimals: 10
});
saveCreatedFeature(this._httpService, layerUrl, geoJSONGeometry, projection)
.then((featureId) => {
this._destroy();
this.#waiter?.resolve(featureId);
})
.catch((err: Error) => {
console.log(err);
this._destroy();
this.#waiter?.reject(err);
});
this._save(feature);
});

// update event handler when container changes
Expand All @@ -199,7 +226,9 @@ export class EditingWorkflowImpl

reset() {
this._drawInteraction.abortDrawing();
this._tooltip.element.textContent = this._intl.formatMessage({ id: "tooltip.begin" });
this._tooltip.element.textContent = this._intl.formatMessage({
id: "create.tooltip.begin"
});
this._setState("active:initialized");
}

Expand All @@ -209,7 +238,7 @@ export class EditingWorkflowImpl
}

private _destroy() {
this._olMap.removeLayer(this._drawLayer);
this._olMap.removeLayer(this._editingLayer);
this._olMap.removeInteraction(this._drawInteraction);
this._tooltip.destroy();

Expand All @@ -225,18 +254,35 @@ export class EditingWorkflowImpl
this._mapContainer?.removeEventListener("keydown", this._enterHandler);
this._mapContainer?.removeEventListener("keydown", this._escapeHandler);

this._state = "inactive";
this._setState("destroyed");
}

whenComplete(): Promise<string | undefined> {
triggerSave() {
// Stop drawing - the `drawend` event is dispatched before inserting the feature.
this._drawInteraction.finishDrawing();
}

whenComplete(): Promise<Record<string, string> | undefined> {
if (this._state === "destroyed") {
if (this._error) {
return Promise.reject(this._error);
} else {
if (this._featureId) {
return Promise.resolve({ featureId: this._featureId });
} else {
return Promise.resolve(undefined);
}
}
}

const manualPromise = (this.#waiter ??= createManualPromise());
return manualPromise.promise;
}

private _createTooltip(olMap: OlMap): Tooltip {
const element = document.createElement("div");
element.className = "editing-tooltip editing-tooltip-hidden";
element.textContent = this._intl.formatMessage({ id: "tooltip.begin" });
element.textContent = this._intl.formatMessage({ id: "create.tooltip.begin" });

const overlay = new Overlay({
element: element,
Expand Down
Loading

0 comments on commit 434fd3e

Please sign in to comment.