From 180306b38c116328bd2f1be50ab01ea374803bef Mon Sep 17 00:00:00 2001 From: santilland Date: Wed, 25 Sep 2024 16:45:17 +0200 Subject: [PATCH 1/9] feat: added initial bounding box format handler for jsonform --- .../src/custom-inputs/bounding-boxes.js | 116 ++++++++++++++++++ elements/jsonform/src/custom-inputs/index.js | 11 ++ elements/jsonform/stories/drawtools.js | 12 ++ elements/jsonform/stories/index.js | 1 + elements/jsonform/stories/jsonform.stories.js | 6 + .../stories/public/drawToolsSchema.json | 17 +++ 6 files changed, 163 insertions(+) create mode 100644 elements/jsonform/src/custom-inputs/bounding-boxes.js create mode 100644 elements/jsonform/stories/drawtools.js create mode 100644 elements/jsonform/stories/public/drawToolsSchema.json diff --git a/elements/jsonform/src/custom-inputs/bounding-boxes.js b/elements/jsonform/src/custom-inputs/bounding-boxes.js new file mode 100644 index 000000000..e9e7c3f08 --- /dev/null +++ b/elements/jsonform/src/custom-inputs/bounding-boxes.js @@ -0,0 +1,116 @@ +import { AbstractEditor } from "@json-editor/json-editor/src/editor.js"; +// import "@eox/drawtools"; + +/** + * Set multiple attributes to an element + * + * @param {Element} element - The DOM element to set attributes on + * @param {{[key: string]: any}} attributes - The attributes to set on the element + */ +function setAttributes(element, attributes) { + Object.keys(attributes).forEach((attr) => { + element.setAttribute(attr, attributes[attr]); + }); +} + +// Define a custom editor class extending AbstractEditor +export class BoundingBoxesEditor extends AbstractEditor { + register() { + super.register(); + } + + unregister() { + super.unregister(); + } + + // Build the editor UI + build() { + // const properties = this.schema.properties; + const options = this.options; + const description = this.schema.description; + const theme = this.theme; + // const startVals = this.defaults.startVals[this.key]; + + // Create label and description elements if not in compact mode + if (!options.compact) + this.header = this.label = theme.getFormInputLabel( + this.getTitle(), + this.isRequired() + ); + if (description) + this.description = theme.getFormInputDescription( + this.translateProperty(description) + ); + if (options.infoText) + this.infoButton = theme.getInfoButton( + this.translateProperty(options.infoText) + ); + + const drawtoolsEl = document.createElement("eox-drawtools"); + + const attributes = { + type: "Box", + }; + if (this.schema.format === "bounding-boxes") { + attributes["multiple-features"] = true; + } + + if ("for" in this.options) { + attributes.for = this.options.for; + } else { + // We need to create a map + const eoxmapEl = document.createElement("eox-map"); + eoxmapEl.projection = "EPSG:4326"; + const mapId = "map-" + this.formname.replace(/[^\w\s]/gi, ""); + eoxmapEl.layers = [{ type: "Tile", source: { type: "OSM" } }]; + setAttributes(eoxmapEl, { + id: mapId, + style: "width: 100%; height: 300px;", + }); + this.container.appendChild(eoxmapEl); + drawtoolsEl.for = "eox-map#" + mapId; + } + setAttributes(drawtoolsEl, attributes); + + this.input = drawtoolsEl; + this.input.id = this.formname; + this.control = theme.getFormControl( + this.label, + this.input, + this.description, + this.infoButton + ); + + if (this.schema.readOnly || this.schema.readonly) { + this.disable(true); + this.input.disabled = true; + } + + // Add event listener for change events on the draw tools + this.input.addEventListener("drawupdate", (e) => { + e.preventDefault(); + e.stopPropagation(); + if (this.schema.format === "bounding-boxes") { + this.value = e.detail.map((val) => { + return val.getGeometry().getExtent(); + }); + } else if (e.detail.length > 0) { + this.value = e.detail[0].getGeometry().getExtent(); + } + this.onChange(true); + }); + + this.container.appendChild(this.control); + } + + // Destroy the editor and remove all associated elements + destroy() { + if (this.label && this.label.parentNode) + this.label.parentNode.removeChild(this.label); + if (this.description && this.description.parentNode) + this.description.parentNode.removeChild(this.description); + if (this.input && this.input.parentNode) + this.input.parentNode.removeChild(this.input); + super.destroy(); + } +} diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index 2f4b1065f..bdb2e2d7a 100644 --- a/elements/jsonform/src/custom-inputs/index.js +++ b/elements/jsonform/src/custom-inputs/index.js @@ -1,5 +1,6 @@ import { JSONEditor } from "@json-editor/json-editor/src/core.js"; import { MinMaxEditor } from "./minmax"; +import { BoundingBoxesEditor } from "./bounding-boxes"; // Define custom input types const inputs = [ @@ -8,6 +9,16 @@ const inputs = [ format: "minmax", func: MinMaxEditor, }, + { + type: "object", + format: "bounding-boxes", + func: BoundingBoxesEditor, + }, + { + type: "object", + format: "bounding-box", + func: BoundingBoxesEditor, + }, ]; /** diff --git a/elements/jsonform/stories/drawtools.js b/elements/jsonform/stories/drawtools.js new file mode 100644 index 000000000..f37b9e853 --- /dev/null +++ b/elements/jsonform/stories/drawtools.js @@ -0,0 +1,12 @@ +/** + * Drawtools component demonstrating the configuration options for eox-jsonform + * It renders drawtools based on json-form config + */ +import drawToolsSchema from "./public/drawToolsSchema.json"; + +const DrawTools = { + args: { + schema: drawToolsSchema, + }, +}; +export default DrawTools; diff --git a/elements/jsonform/stories/index.js b/elements/jsonform/stories/index.js index 9561faef6..699f476ae 100644 --- a/elements/jsonform/stories/index.js +++ b/elements/jsonform/stories/index.js @@ -4,3 +4,4 @@ export { default as CollectionStory } from "./collection"; // Input form based o export { default as ExternalStory } from "./external"; // Input form based on External URL export { default as MarkdownStory } from "./markdown"; // Input form based on Markdown Editor config export { default as UnStyledStory } from "./unstyled"; // Unstyled input form +export { default as DrawToolsStory } from "./drawtools"; // Input form based on drawtools diff --git a/elements/jsonform/stories/jsonform.stories.js b/elements/jsonform/stories/jsonform.stories.js index 63755ea5f..764ba4e5f 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -7,6 +7,7 @@ import { MarkdownStory, PrimaryStory, UnStyledStory, + DrawToolsStory, } from "./index.js"; export default { @@ -52,3 +53,8 @@ export const Markdown = MarkdownStory; * Unstyled JSON Form */ export const Unstyled = UnStyledStory; + +/** + * Unstyled JSON Form + */ +export const DrawTools = DrawToolsStory; diff --git a/elements/jsonform/stories/public/drawToolsSchema.json b/elements/jsonform/stories/public/drawToolsSchema.json new file mode 100644 index 000000000..bef4d0b83 --- /dev/null +++ b/elements/jsonform/stories/public/drawToolsSchema.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "bboxes": { + "title": "Multi bbox example", + "type": "object", + "properties": {}, + "format": "bounding-boxes" + }, + "bbox": { + "title": "Single bbox example", + "type": "object", + "properties": {}, + "format": "bounding-box" + } + } +} From c5d7aafaa47e634084c81e6d7c6767d5b437946c Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Wed, 9 Oct 2024 16:42:38 +0200 Subject: [PATCH 2/9] feat: add polygons and editors --- elements/jsonform/src/custom-inputs/index.js | 36 +++++++++++++++++-- .../{bounding-boxes.js => spatial.js} | 33 ++++++++++++----- elements/jsonform/stories/index.js | 1 + elements/jsonform/stories/jsonform.stories.js | 6 ++++ elements/jsonform/stories/polygons.js | 12 +++++++ .../stories/public/drawToolsSchema.json | 12 +++++++ .../stories/public/polygonSchema.json | 30 ++++++++++++++++ package-lock.json | 16 ++++----- 8 files changed, 126 insertions(+), 20 deletions(-) rename elements/jsonform/src/custom-inputs/{bounding-boxes.js => spatial.js} (81%) create mode 100644 elements/jsonform/stories/polygons.js create mode 100644 elements/jsonform/stories/public/polygonSchema.json diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index bdb2e2d7a..afd2de489 100644 --- a/elements/jsonform/src/custom-inputs/index.js +++ b/elements/jsonform/src/custom-inputs/index.js @@ -1,6 +1,6 @@ import { JSONEditor } from "@json-editor/json-editor/src/core.js"; import { MinMaxEditor } from "./minmax"; -import { BoundingBoxesEditor } from "./bounding-boxes"; +import { SpatialEditor } from "./spatial"; // Define custom input types const inputs = [ @@ -12,12 +12,42 @@ const inputs = [ { type: "object", format: "bounding-boxes", - func: BoundingBoxesEditor, + func: SpatialEditor, }, { type: "object", format: "bounding-box", - func: BoundingBoxesEditor, + func: SpatialEditor, + }, + { + type: "object", + format: "bounding-boxes-editor", + func: SpatialEditor, + }, + { + type: "object", + format: "bounding-box-editor", + func: SpatialEditor, + }, + { + type: "object", + format: "polygons", + func: SpatialEditor, + }, + { + type: "object", + format: "polygon", + func: SpatialEditor, + }, + { + type: "object", + format: "polygons-editor", + func: SpatialEditor, + }, + { + type: "object", + format: "polygon-editor", + func: SpatialEditor, }, ]; diff --git a/elements/jsonform/src/custom-inputs/bounding-boxes.js b/elements/jsonform/src/custom-inputs/spatial.js similarity index 81% rename from elements/jsonform/src/custom-inputs/bounding-boxes.js rename to elements/jsonform/src/custom-inputs/spatial.js index e9e7c3f08..7ed5ea6aa 100644 --- a/elements/jsonform/src/custom-inputs/bounding-boxes.js +++ b/elements/jsonform/src/custom-inputs/spatial.js @@ -14,14 +14,14 @@ function setAttributes(element, attributes) { } // Define a custom editor class extending AbstractEditor -export class BoundingBoxesEditor extends AbstractEditor { - register() { - super.register(); - } +export class SpatialEditor extends AbstractEditor { + // register() { + // super.register(); + // } - unregister() { - super.unregister(); - } + // unregister() { + // super.unregister(); + // } // Build the editor UI build() { @@ -48,13 +48,27 @@ export class BoundingBoxesEditor extends AbstractEditor { const drawtoolsEl = document.createElement("eox-drawtools"); + const isPolygon = ["polygon", "polygons"].some(p => this.schema.format.includes(p)) + const isMulti = ["bounding-boxes", "polygons"].some(m => this.schema.format.includes(m)) + const enableEditor = this.schema.format.includes("editor") + + const drawType = isPolygon ? "Polygon" : "Box" const attributes = { - type: "Box", + type: drawType }; - if (this.schema.format === "bounding-boxes") { + if (isMulti) { attributes["multiple-features"] = true; } + if (enableEditor) { + attributes["import-features"] = true + attributes["show-editor"] = true + if (isMulti) { + attributes["show-list"] = true + } + console.log("🚀 ~ SpatialEditor ~ build ~ isMulti:", this.formname, isMulti) + } + if ("for" in this.options) { attributes.for = this.options.for; } else { @@ -63,6 +77,7 @@ export class BoundingBoxesEditor extends AbstractEditor { eoxmapEl.projection = "EPSG:4326"; const mapId = "map-" + this.formname.replace(/[^\w\s]/gi, ""); eoxmapEl.layers = [{ type: "Tile", source: { type: "OSM" } }]; + setAttributes(eoxmapEl, { id: mapId, style: "width: 100%; height: 300px;", diff --git a/elements/jsonform/stories/index.js b/elements/jsonform/stories/index.js index 699f476ae..a76c9a813 100644 --- a/elements/jsonform/stories/index.js +++ b/elements/jsonform/stories/index.js @@ -5,3 +5,4 @@ export { default as ExternalStory } from "./external"; // Input form based on Ex export { default as MarkdownStory } from "./markdown"; // Input form based on Markdown Editor config export { default as UnStyledStory } from "./unstyled"; // Unstyled input form export { default as DrawToolsStory } from "./drawtools"; // Input form based on drawtools +export { default as PolygonStory } from "./polygons"; // Input form based on drawtools diff --git a/elements/jsonform/stories/jsonform.stories.js b/elements/jsonform/stories/jsonform.stories.js index 764ba4e5f..a785aac56 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -8,6 +8,7 @@ import { PrimaryStory, UnStyledStory, DrawToolsStory, + PolygonStory, } from "./index.js"; export default { @@ -58,3 +59,8 @@ export const Unstyled = UnStyledStory; * Unstyled JSON Form */ export const DrawTools = DrawToolsStory; + +/** + * Unstyled JSON Form + */ +export const Polygons = PolygonStory; \ No newline at end of file diff --git a/elements/jsonform/stories/polygons.js b/elements/jsonform/stories/polygons.js new file mode 100644 index 000000000..09c230308 --- /dev/null +++ b/elements/jsonform/stories/polygons.js @@ -0,0 +1,12 @@ +/** + * Drawtools component demonstrating the configuration options for eox-jsonform + * It renders drawtools based on json-form config + */ +import polygonsScheme from "./public/polygonSchema.json"; + +const Polygons = { + args: { + schema:polygonsScheme, + }, +}; +export default Polygons; \ No newline at end of file diff --git a/elements/jsonform/stories/public/drawToolsSchema.json b/elements/jsonform/stories/public/drawToolsSchema.json index bef4d0b83..342c75b82 100644 --- a/elements/jsonform/stories/public/drawToolsSchema.json +++ b/elements/jsonform/stories/public/drawToolsSchema.json @@ -12,6 +12,18 @@ "type": "object", "properties": {}, "format": "bounding-box" + }, + "bboxes-editor": { + "title": "Multi bbox example + Editor", + "type": "object", + "properties": {}, + "format": "bounding-boxes-editor" + }, + "bbox-editor": { + "title": "Single bbox example + Editor", + "type": "object", + "properties": {}, + "format": "bounding-box-editor" } } } diff --git a/elements/jsonform/stories/public/polygonSchema.json b/elements/jsonform/stories/public/polygonSchema.json new file mode 100644 index 000000000..4b2a4c106 --- /dev/null +++ b/elements/jsonform/stories/public/polygonSchema.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "properties": { + "polygons": { + "title": "Multi polygon", + "type": "object", + "properties": {}, + "format": "polygons" + }, + "polygon": { + "title": "Single polygon", + "type": "object", + "properties": {}, + "format": "polygon" + }, + "polygons-editor": { + "title": "Multi polygon + Editor", + "type": "object", + "properties": {}, + "format": "polygons-editor" + }, + "polygon-editor": { + "title": "Single polygon + Editor", + "type": "object", + "properties": {}, + "format": "polygon-editor" + } + } + } + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2c1e7bdf4..6d8b3dcdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,7 +102,7 @@ }, "elements/itemfilter": { "name": "@eox/itemfilter", - "version": "1.1.0", + "version": "1.1.1", "dependencies": { "@floating-ui/dom": "^1.6.8", "@turf/boolean-intersects": "^7.0.0", @@ -153,7 +153,7 @@ }, "elements/layercontrol": { "name": "@eox/layercontrol", - "version": "0.20.0", + "version": "0.21.0", "dependencies": { "dayjs": "^1.11.8", "lit": "^3.0.2", @@ -162,9 +162,9 @@ }, "devDependencies": { "@eox/eslint-config": "^1.0.0", - "@eox/jsonform": "*", - "@eox/map": "*", - "@eox/timecontrol": "*", + "@eox/jsonform": "latest", + "@eox/map": "latest", + "@eox/timecontrol": "latest", "@types/sortablejs": "^1.15.1", "@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/parser": "^7.0.1", @@ -235,7 +235,7 @@ }, "elements/map": { "name": "@eox/map", - "version": "1.13.0", + "version": "1.13.1", "dependencies": { "lit": "^3.0.2", "ol": "^10.2.0", @@ -279,7 +279,7 @@ }, "elements/storytelling": { "name": "@eox/storytelling", - "version": "1.0.8", + "version": "1.1.0", "dependencies": { "@sindresorhus/slugify": "^2.2.1", "glightbox": "^3.3.0", @@ -305,7 +305,7 @@ }, "elements/timecontrol": { "name": "@eox/timecontrol", - "version": "0.7.2", + "version": "0.8.0", "dependencies": { "dayjs": "^1.11.9", "lit": "^3.0.2", From 2dfe5c79fb898b7b87baf010218df09d7b7a686b Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Wed, 9 Oct 2024 16:50:45 +0200 Subject: [PATCH 3/9] chore: cleanup --- .../jsonform/src/custom-inputs/spatial.js | 21 ++++---- elements/jsonform/stories/jsonform.stories.js | 2 +- elements/jsonform/stories/polygons.js | 4 +- .../stories/public/polygonSchema.json | 53 +++++++++---------- 4 files changed, 41 insertions(+), 39 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial.js b/elements/jsonform/src/custom-inputs/spatial.js index 7ed5ea6aa..857f1a4e1 100644 --- a/elements/jsonform/src/custom-inputs/spatial.js +++ b/elements/jsonform/src/custom-inputs/spatial.js @@ -48,25 +48,28 @@ export class SpatialEditor extends AbstractEditor { const drawtoolsEl = document.createElement("eox-drawtools"); - const isPolygon = ["polygon", "polygons"].some(p => this.schema.format.includes(p)) - const isMulti = ["bounding-boxes", "polygons"].some(m => this.schema.format.includes(m)) - const enableEditor = this.schema.format.includes("editor") + const isPolygon = ["polygon", "polygons"].some((p) => + this.schema.format.includes(p) + ); + const isMulti = ["bounding-boxes", "polygons"].some((m) => + this.schema.format.includes(m) + ); + const enableEditor = this.schema.format.includes("editor"); - const drawType = isPolygon ? "Polygon" : "Box" + const drawType = isPolygon ? "Polygon" : "Box"; const attributes = { - type: drawType + type: drawType, }; if (isMulti) { attributes["multiple-features"] = true; } if (enableEditor) { - attributes["import-features"] = true - attributes["show-editor"] = true + attributes["import-features"] = true; + attributes["show-editor"] = true; if (isMulti) { - attributes["show-list"] = true + attributes["show-list"] = true; } - console.log("🚀 ~ SpatialEditor ~ build ~ isMulti:", this.formname, isMulti) } if ("for" in this.options) { diff --git a/elements/jsonform/stories/jsonform.stories.js b/elements/jsonform/stories/jsonform.stories.js index a785aac56..12a191173 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -63,4 +63,4 @@ export const DrawTools = DrawToolsStory; /** * Unstyled JSON Form */ -export const Polygons = PolygonStory; \ No newline at end of file +export const Polygons = PolygonStory; diff --git a/elements/jsonform/stories/polygons.js b/elements/jsonform/stories/polygons.js index 09c230308..b0203969a 100644 --- a/elements/jsonform/stories/polygons.js +++ b/elements/jsonform/stories/polygons.js @@ -6,7 +6,7 @@ import polygonsScheme from "./public/polygonSchema.json"; const Polygons = { args: { - schema:polygonsScheme, + schema: polygonsScheme, }, }; -export default Polygons; \ No newline at end of file +export default Polygons; diff --git a/elements/jsonform/stories/public/polygonSchema.json b/elements/jsonform/stories/public/polygonSchema.json index 4b2a4c106..3f4cf005b 100644 --- a/elements/jsonform/stories/public/polygonSchema.json +++ b/elements/jsonform/stories/public/polygonSchema.json @@ -1,30 +1,29 @@ { - "type": "object", - "properties": { - "polygons": { - "title": "Multi polygon", - "type": "object", - "properties": {}, - "format": "polygons" - }, - "polygon": { - "title": "Single polygon", - "type": "object", - "properties": {}, - "format": "polygon" - }, - "polygons-editor": { - "title": "Multi polygon + Editor", - "type": "object", - "properties": {}, - "format": "polygons-editor" - }, - "polygon-editor": { - "title": "Single polygon + Editor", - "type": "object", - "properties": {}, - "format": "polygon-editor" - } + "type": "object", + "properties": { + "polygons": { + "title": "Multi polygon", + "type": "object", + "properties": {}, + "format": "polygons" + }, + "polygon": { + "title": "Single polygon", + "type": "object", + "properties": {}, + "format": "polygon" + }, + "polygons-editor": { + "title": "Multi polygon + Editor", + "type": "object", + "properties": {}, + "format": "polygons-editor" + }, + "polygon-editor": { + "title": "Single polygon + Editor", + "type": "object", + "properties": {}, + "format": "polygon-editor" } } - \ No newline at end of file +} From 7f3a774074b21be1de7b9a57e6639a165061286e Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Tue, 22 Oct 2024 15:26:34 +0200 Subject: [PATCH 4/9] feat: selection input + adjusted returned values accordingly --- elements/jsonform/src/custom-inputs/index.js | 10 +++ .../jsonform/src/custom-inputs/spatial.js | 81 ++++++++++++++----- 2 files changed, 72 insertions(+), 19 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index afd2de489..faf66ab2c 100644 --- a/elements/jsonform/src/custom-inputs/index.js +++ b/elements/jsonform/src/custom-inputs/index.js @@ -49,6 +49,16 @@ const inputs = [ format: "polygon-editor", func: SpatialEditor, }, + { + type: "object", + format: "feature", + func: SpatialEditor, + }, + { + type: "object", + format: "features", + func: SpatialEditor, + }, ]; /** diff --git a/elements/jsonform/src/custom-inputs/spatial.js b/elements/jsonform/src/custom-inputs/spatial.js index 8fb222711..d5d94d90f 100644 --- a/elements/jsonform/src/custom-inputs/spatial.js +++ b/elements/jsonform/src/custom-inputs/spatial.js @@ -48,10 +48,19 @@ export class SpatialEditor extends AbstractEditor { const drawtoolsEl = document.createElement("eox-drawtools"); + const isSelection = ["feature", "features"].some((f) => + this.schema.format.includes(f) + ); + const isPolygon = ["polygon", "polygons"].some((p) => this.schema.format.includes(p) ); - const isMulti = ["bounding-boxes", "polygons"].some((m) => + + const isBox = ["bounding-boxes", "bounding-box"].some((p) => + this.schema.format.includes(p) + ); + + const isMulti = ["bounding-boxes", "polygons", "features"].some((m) => this.schema.format.includes(m) ); const enableEditor = this.schema.format.includes("editor"); @@ -60,6 +69,9 @@ export class SpatialEditor extends AbstractEditor { const attributes = { type: drawType, }; + if (isSelection) { + attributes["layer-id"] = this.schema.options.layerId; + } if (isMulti) { attributes["multiple-features"] = true; } @@ -67,12 +79,13 @@ export class SpatialEditor extends AbstractEditor { if (enableEditor) { attributes["import-features"] = true; attributes["show-editor"] = true; - if (isMulti) { - attributes["show-list"] = true; - } + } + + if (isMulti) { + attributes["show-list"] = true; } - if ("for" in this.options) { + if ("for" in (this.schema.options ?? {})) { attributes.for = this.options.for; } else { // We need to create a map @@ -86,7 +99,7 @@ export class SpatialEditor extends AbstractEditor { style: "width: 100%; height: 300px;", }); this.container.appendChild(eoxmapEl); - drawtoolsEl.for = "eox-map#" + mapId; + attributes.for = "eox-map#" + mapId; } setAttributes(drawtoolsEl, attributes); @@ -105,18 +118,46 @@ export class SpatialEditor extends AbstractEditor { } // Add event listener for change events on the draw tools - this.input.addEventListener("drawupdate", (e) => { - e.preventDefault(); - e.stopPropagation(); - if (this.schema.format === "bounding-boxes") { - this.value = e.detail.map((val) => { - return val.getGeometry().getExtent(); - }); - } else if (e.detail.length > 0) { - this.value = e.detail[0].getGeometry().getExtent(); - } - this.onChange(true); - }); + //@ts-expect-error + this.input.addEventListener("drawupdate",/** + * @param {CustomEvent} e + */ + (e) => { + e.preventDefault(); + e.stopPropagation(); + + switch (true) { + case (!e.detail || e.detail?.length === 0): { + this.value = null; + break + } + case isSelection: { + /** @param {import("ol/Feature").default} feature */ + const getProperty = (feature) => + feature.get(this.schema.options.featureProperty) ?? + feature; + this.value = e.detail.length + ? e.detail.map(getProperty) + : getProperty(e.detail); + break + } + case isBox: { + /** @param {import("ol/Feature").default} feature */ + const getExtent = (feature) => feature.getGeometry().getExtent(); + this.value = e.detail?.length + ? e.detail.map(getExtent) + : getExtent(e.detail); + break + } + case isPolygon: + this.value = e.detail; + break + default: + break; + } + + this.onChange(true); + }); this.container.appendChild(this.control); } @@ -127,8 +168,10 @@ export class SpatialEditor extends AbstractEditor { this.label.parentNode.removeChild(this.label); if (this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description); - if (this.input && this.input.parentNode) + if (this.input && this.input.parentNode) { this.input.parentNode.removeChild(this.input); + this.input.remove(); + } super.destroy(); } } From 6bf73759a53f0d12135f674489e4462df5acb60d Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Tue, 22 Oct 2024 15:29:18 +0200 Subject: [PATCH 5/9] fix: feature selection story & renamed drawtool to bbox --- elements/jsonform/src/enums/index.js | 1 + elements/jsonform/src/enums/stories.js | 44 +++++++++++++++++++ elements/jsonform/stories/bounding-box.js | 12 +++++ elements/jsonform/stories/drawtools.js | 12 ----- .../jsonform/stories/feature-selection.js | 36 +++++++++++++++ elements/jsonform/stories/index.js | 5 ++- elements/jsonform/stories/jsonform.stories.js | 18 +++++--- elements/jsonform/stories/polygons.js | 4 +- ...oolsSchema.json => boundingBoxSchema.json} | 0 .../stories/public/featureSchema.json | 24 ++++++++++ package-lock.json | 8 ++-- 11 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 elements/jsonform/src/enums/stories.js create mode 100644 elements/jsonform/stories/bounding-box.js delete mode 100644 elements/jsonform/stories/drawtools.js create mode 100644 elements/jsonform/stories/feature-selection.js rename elements/jsonform/stories/public/{drawToolsSchema.json => boundingBoxSchema.json} (100%) create mode 100644 elements/jsonform/stories/public/featureSchema.json diff --git a/elements/jsonform/src/enums/index.js b/elements/jsonform/src/enums/index.js index 526db404c..c8236dcab 100644 --- a/elements/jsonform/src/enums/index.js +++ b/elements/jsonform/src/enums/index.js @@ -1 +1,2 @@ export { TEST_SELECTORS } from "./test"; +export {STORIES_BlUE_VECTOR_LAYERS,STORIES_GREY_VECTOR_LAYERS,STORIES_MAP_STYLE } from "./stories" diff --git a/elements/jsonform/src/enums/stories.js b/elements/jsonform/src/enums/stories.js new file mode 100644 index 000000000..1741bcc46 --- /dev/null +++ b/elements/jsonform/src/enums/stories.js @@ -0,0 +1,44 @@ +export const STORIES_MAP_STYLE = + "width: 100%; height: 300px; margin: 7px;"; + +export const STORIES_GREY_VECTOR_LAYERS = [ + { + type: "Vector", + background: "lightgrey", + properties: { + id: "regions-grey", + }, + source: { + type: "Vector", + url: "https://openlayers.org/data/vector/ecoregions.json", + format: "GeoJSON", + attributions: "Regions: @ openlayers.org", + }, + style: { + "stroke-color": "black", + "stroke-width": 1, + "fill-color": "darkgrey", + }, + }, + ]; + + export const STORIES_BlUE_VECTOR_LAYERS = [ + { + type: "Vector", + background: "lightgrey", + properties: { + id: "regions-blue", + }, + source: { + type: "Vector", + url: "https://openlayers.org/data/vector/ecoregions.json", + format: "GeoJSON", + attributions: "Regions: @ openlayers.org", + }, + style: { + "stroke-color": "black", + "stroke-width": 1, + "fill-color": "lightblue", + }, + }, + ]; \ No newline at end of file diff --git a/elements/jsonform/stories/bounding-box.js b/elements/jsonform/stories/bounding-box.js new file mode 100644 index 000000000..3068264a7 --- /dev/null +++ b/elements/jsonform/stories/bounding-box.js @@ -0,0 +1,12 @@ +/** + * Drawtools component demonstrating the configuration options for eox-jsonform + * Allows users to select a bounding box on a map as a form input + */ +import boundingBoxSchema from "./public/boundingBoxSchema.json"; + +const BoundingBox = { + args: { + schema: boundingBoxSchema, + }, +}; +export default BoundingBox; diff --git a/elements/jsonform/stories/drawtools.js b/elements/jsonform/stories/drawtools.js deleted file mode 100644 index f37b9e853..000000000 --- a/elements/jsonform/stories/drawtools.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Drawtools component demonstrating the configuration options for eox-jsonform - * It renders drawtools based on json-form config - */ -import drawToolsSchema from "./public/drawToolsSchema.json"; - -const DrawTools = { - args: { - schema: drawToolsSchema, - }, -}; -export default DrawTools; diff --git a/elements/jsonform/stories/feature-selection.js b/elements/jsonform/stories/feature-selection.js new file mode 100644 index 000000000..ea28ad647 --- /dev/null +++ b/elements/jsonform/stories/feature-selection.js @@ -0,0 +1,36 @@ +import { html } from "lit"; +import { STORIES_MAP_STYLE, STORIES_BlUE_VECTOR_LAYERS, STORIES_GREY_VECTOR_LAYERS } from "../src/enums/stories" +/** + * Drawtools component demonstrating the configuration options for eox-jsonform + * Allows user to select a feature from an external eox-map as an input + */ +import featureSchema from "./public/featureSchema.json"; + +const FeatureSelection = { + args: { + schema: featureSchema, + onChange: (e) => console.log("change event", e.detail), + }, + render: (args) => html` + + + + + + `, +}; +export default FeatureSelection; diff --git a/elements/jsonform/stories/index.js b/elements/jsonform/stories/index.js index a76c9a813..e6bfaa61c 100644 --- a/elements/jsonform/stories/index.js +++ b/elements/jsonform/stories/index.js @@ -4,5 +4,6 @@ export { default as CollectionStory } from "./collection"; // Input form based o export { default as ExternalStory } from "./external"; // Input form based on External URL export { default as MarkdownStory } from "./markdown"; // Input form based on Markdown Editor config export { default as UnStyledStory } from "./unstyled"; // Unstyled input form -export { default as DrawToolsStory } from "./drawtools"; // Input form based on drawtools -export { default as PolygonStory } from "./polygons"; // Input form based on drawtools +export { default as BoundingBoxStory } from "./bounding-box"; // Input form based on drawtools - Box +export { default as PolygonStory } from "./polygons"; // Input form based on drawtools - Polygon +export { default as FeatureSelectionStory } from "./feature-selection"; // Input form based on drawtools - Feature Selection diff --git a/elements/jsonform/stories/jsonform.stories.js b/elements/jsonform/stories/jsonform.stories.js index 12a191173..fb528a8bf 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -7,8 +7,9 @@ import { MarkdownStory, PrimaryStory, UnStyledStory, - DrawToolsStory, + BoundingBoxStory, PolygonStory, + FeatureSelectionStory, } from "./index.js"; export default { @@ -51,16 +52,21 @@ export const External = ExternalStory; export const Markdown = MarkdownStory; /** - * Unstyled JSON Form + * JSON Form based on drawtools - Box */ -export const Unstyled = UnStyledStory; +export const BoundigBox = BoundingBoxStory; /** - * Unstyled JSON Form + * JSON Form based on drawtools - Polygon */ -export const DrawTools = DrawToolsStory; +export const Polygons = PolygonStory; +/** + * JSON Form based on drawtools - Feature Selection + */ +export const FeatureSelection = FeatureSelectionStory; /** * Unstyled JSON Form */ -export const Polygons = PolygonStory; +export const Unstyled = UnStyledStory; + diff --git a/elements/jsonform/stories/polygons.js b/elements/jsonform/stories/polygons.js index b0203969a..41e61f067 100644 --- a/elements/jsonform/stories/polygons.js +++ b/elements/jsonform/stories/polygons.js @@ -1,6 +1,6 @@ /** - * Drawtools component demonstrating the configuration options for eox-jsonform - * It renders drawtools based on json-form config + * Drawtools component demonstrating the configuration options for eox-jsonform. + * Allows the user to draw polygons on the map as an input */ import polygonsScheme from "./public/polygonSchema.json"; diff --git a/elements/jsonform/stories/public/drawToolsSchema.json b/elements/jsonform/stories/public/boundingBoxSchema.json similarity index 100% rename from elements/jsonform/stories/public/drawToolsSchema.json rename to elements/jsonform/stories/public/boundingBoxSchema.json diff --git a/elements/jsonform/stories/public/featureSchema.json b/elements/jsonform/stories/public/featureSchema.json new file mode 100644 index 000000000..d009ab420 --- /dev/null +++ b/elements/jsonform/stories/public/featureSchema.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "properties": { + "features": { + "title": "Multi Features", + "type": "object", + "options": { + "layerId": "regions-grey", + "featureProperty": "BIOME_NAME", + "for": "eox-map#first" + }, + "format": "features" + }, + "feature": { + "title": "Feature", + "type": "object", + "options": { + "layerId": "regions-blue", + "for":"eox-map#second" + }, + "format": "feature" + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6bf145f4f..52d1f7a83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ }, "elements/drawtools": { "name": "@eox/drawtools", - "version": "0.10.0", + "version": "0.11.0", "dependencies": { "@eox/elements-utils": "^0.0.1", "lit": "^3.0.2" @@ -153,9 +153,9 @@ "wms-capabilities": "^0.6.0" }, "devDependencies": { - "@eox/jsonform": "*", - "@eox/map": "*", - "@eox/timecontrol": "*", + "@eox/jsonform": "latest", + "@eox/map": "latest", + "@eox/timecontrol": "latest", "@types/sortablejs": "^1.15.1", "ol": "^10.0.0", "sortablejs": "^1.15.0", From ca762faff7270c071c851ffd21ebd04d3f2fa3c7 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Tue, 22 Oct 2024 15:32:33 +0200 Subject: [PATCH 6/9] chore: format --- .../jsonform/src/custom-inputs/spatial.js | 41 ++++++----- elements/jsonform/src/enums/index.js | 6 +- elements/jsonform/src/enums/stories.js | 73 +++++++++---------- .../jsonform/stories/feature-selection.js | 8 +- elements/jsonform/stories/jsonform.stories.js | 1 - .../stories/public/featureSchema.json | 4 +- 6 files changed, 70 insertions(+), 63 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial.js b/elements/jsonform/src/custom-inputs/spatial.js index d5d94d90f..e666980f1 100644 --- a/elements/jsonform/src/custom-inputs/spatial.js +++ b/elements/jsonform/src/custom-inputs/spatial.js @@ -35,33 +35,33 @@ export class SpatialEditor extends AbstractEditor { if (!options.compact) this.header = this.label = theme.getFormInputLabel( this.getTitle(), - this.isRequired() + this.isRequired(), ); if (description) this.description = theme.getFormInputDescription( - this.translateProperty(description) + this.translateProperty(description), ); if (options.infoText) this.infoButton = theme.getInfoButton( - this.translateProperty(options.infoText) + this.translateProperty(options.infoText), ); const drawtoolsEl = document.createElement("eox-drawtools"); const isSelection = ["feature", "features"].some((f) => - this.schema.format.includes(f) + this.schema.format.includes(f), ); const isPolygon = ["polygon", "polygons"].some((p) => - this.schema.format.includes(p) + this.schema.format.includes(p), ); const isBox = ["bounding-boxes", "bounding-box"].some((p) => - this.schema.format.includes(p) + this.schema.format.includes(p), ); const isMulti = ["bounding-boxes", "polygons", "features"].some((m) => - this.schema.format.includes(m) + this.schema.format.includes(m), ); const enableEditor = this.schema.format.includes("editor"); @@ -80,7 +80,7 @@ export class SpatialEditor extends AbstractEditor { attributes["import-features"] = true; attributes["show-editor"] = true; } - + if (isMulti) { attributes["show-list"] = true; } @@ -109,7 +109,7 @@ export class SpatialEditor extends AbstractEditor { this.label, this.input, this.description, - this.infoButton + this.infoButton, ); if (this.schema.readOnly || this.schema.readonly) { @@ -119,27 +119,27 @@ export class SpatialEditor extends AbstractEditor { // Add event listener for change events on the draw tools //@ts-expect-error - this.input.addEventListener("drawupdate",/** - * @param {CustomEvent} e - */ + this.input.addEventListener( + "drawupdate" /** + * @param {CustomEvent} e + */, (e) => { e.preventDefault(); e.stopPropagation(); switch (true) { - case (!e.detail || e.detail?.length === 0): { + case !e.detail || e.detail?.length === 0: { this.value = null; - break + break; } case isSelection: { /** @param {import("ol/Feature").default} feature */ const getProperty = (feature) => - feature.get(this.schema.options.featureProperty) ?? - feature; + feature.get(this.schema.options.featureProperty) ?? feature; this.value = e.detail.length ? e.detail.map(getProperty) : getProperty(e.detail); - break + break; } case isBox: { /** @param {import("ol/Feature").default} feature */ @@ -147,17 +147,18 @@ export class SpatialEditor extends AbstractEditor { this.value = e.detail?.length ? e.detail.map(getExtent) : getExtent(e.detail); - break + break; } case isPolygon: this.value = e.detail; - break + break; default: break; } this.onChange(true); - }); + }, + ); this.container.appendChild(this.control); } diff --git a/elements/jsonform/src/enums/index.js b/elements/jsonform/src/enums/index.js index c8236dcab..dc6fe499d 100644 --- a/elements/jsonform/src/enums/index.js +++ b/elements/jsonform/src/enums/index.js @@ -1,2 +1,6 @@ export { TEST_SELECTORS } from "./test"; -export {STORIES_BlUE_VECTOR_LAYERS,STORIES_GREY_VECTOR_LAYERS,STORIES_MAP_STYLE } from "./stories" +export { + STORIES_BlUE_VECTOR_LAYERS, + STORIES_GREY_VECTOR_LAYERS, + STORIES_MAP_STYLE, +} from "./stories"; diff --git a/elements/jsonform/src/enums/stories.js b/elements/jsonform/src/enums/stories.js index 1741bcc46..c3a665741 100644 --- a/elements/jsonform/src/enums/stories.js +++ b/elements/jsonform/src/enums/stories.js @@ -1,44 +1,43 @@ -export const STORIES_MAP_STYLE = - "width: 100%; height: 300px; margin: 7px;"; +export const STORIES_MAP_STYLE = "width: 100%; height: 300px; margin: 7px;"; export const STORIES_GREY_VECTOR_LAYERS = [ - { + { + type: "Vector", + background: "lightgrey", + properties: { + id: "regions-grey", + }, + source: { type: "Vector", - background: "lightgrey", - properties: { - id: "regions-grey", - }, - source: { - type: "Vector", - url: "https://openlayers.org/data/vector/ecoregions.json", - format: "GeoJSON", - attributions: "Regions: @ openlayers.org", - }, - style: { - "stroke-color": "black", - "stroke-width": 1, - "fill-color": "darkgrey", - }, + url: "https://openlayers.org/data/vector/ecoregions.json", + format: "GeoJSON", + attributions: "Regions: @ openlayers.org", + }, + style: { + "stroke-color": "black", + "stroke-width": 1, + "fill-color": "darkgrey", }, - ]; + }, +]; - export const STORIES_BlUE_VECTOR_LAYERS = [ - { +export const STORIES_BlUE_VECTOR_LAYERS = [ + { + type: "Vector", + background: "lightgrey", + properties: { + id: "regions-blue", + }, + source: { type: "Vector", - background: "lightgrey", - properties: { - id: "regions-blue", - }, - source: { - type: "Vector", - url: "https://openlayers.org/data/vector/ecoregions.json", - format: "GeoJSON", - attributions: "Regions: @ openlayers.org", - }, - style: { - "stroke-color": "black", - "stroke-width": 1, - "fill-color": "lightblue", - }, + url: "https://openlayers.org/data/vector/ecoregions.json", + format: "GeoJSON", + attributions: "Regions: @ openlayers.org", + }, + style: { + "stroke-color": "black", + "stroke-width": 1, + "fill-color": "lightblue", }, - ]; \ No newline at end of file + }, +]; diff --git a/elements/jsonform/stories/feature-selection.js b/elements/jsonform/stories/feature-selection.js index ea28ad647..5b3aafd14 100644 --- a/elements/jsonform/stories/feature-selection.js +++ b/elements/jsonform/stories/feature-selection.js @@ -1,5 +1,9 @@ import { html } from "lit"; -import { STORIES_MAP_STYLE, STORIES_BlUE_VECTOR_LAYERS, STORIES_GREY_VECTOR_LAYERS } from "../src/enums/stories" +import { + STORIES_MAP_STYLE, + STORIES_BlUE_VECTOR_LAYERS, + STORIES_GREY_VECTOR_LAYERS, +} from "../src/enums"; /** * Drawtools component demonstrating the configuration options for eox-jsonform * Allows user to select a feature from an external eox-map as an input @@ -21,7 +25,7 @@ const FeatureSelection = { diff --git a/elements/jsonform/stories/jsonform.stories.js b/elements/jsonform/stories/jsonform.stories.js index fb528a8bf..66ee385f2 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -69,4 +69,3 @@ export const FeatureSelection = FeatureSelectionStory; * Unstyled JSON Form */ export const Unstyled = UnStyledStory; - diff --git a/elements/jsonform/stories/public/featureSchema.json b/elements/jsonform/stories/public/featureSchema.json index d009ab420..11f6ba4d1 100644 --- a/elements/jsonform/stories/public/featureSchema.json +++ b/elements/jsonform/stories/public/featureSchema.json @@ -16,9 +16,9 @@ "type": "object", "options": { "layerId": "regions-blue", - "for":"eox-map#second" + "for": "eox-map#second" }, "format": "feature" } } -} \ No newline at end of file +} From e20e4ece582ddad28d9f8d8035606303d9bccf50 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Tue, 22 Oct 2024 16:06:11 +0200 Subject: [PATCH 7/9] fix: lint & tests --- .../jsonform/src/custom-inputs/spatial.js | 76 ++++++++++--------- .../test/fixtures/collectionSchema.json | 8 +- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial.js b/elements/jsonform/src/custom-inputs/spatial.js index e666980f1..b338daa06 100644 --- a/elements/jsonform/src/custom-inputs/spatial.js +++ b/elements/jsonform/src/custom-inputs/spatial.js @@ -118,46 +118,48 @@ export class SpatialEditor extends AbstractEditor { } // Add event listener for change events on the draw tools - //@ts-expect-error this.input.addEventListener( - "drawupdate" /** - * @param {CustomEvent} e - */, - (e) => { - e.preventDefault(); - e.stopPropagation(); - - switch (true) { - case !e.detail || e.detail?.length === 0: { - this.value = null; - break; + "drawupdate", + /** @type {EventListener} */ ( + /** + * @param {CustomEvent} e + */ + (e) => { + e.preventDefault(); + e.stopPropagation(); + + switch (true) { + case !e.detail || e.detail?.length === 0: { + this.value = null; + break; + } + case isSelection: { + /** @param {import("ol/Feature").default} feature */ + const getProperty = (feature) => + feature.get(this.schema.options.featureProperty) ?? feature; + this.value = e.detail.length + ? e.detail.map(getProperty) + : getProperty(e.detail); + break; + } + case isBox: { + /** @param {import("ol/Feature").default} feature */ + const getExtent = (feature) => feature.getGeometry().getExtent(); + this.value = e.detail?.length + ? e.detail.map(getExtent) + : getExtent(e.detail); + break; + } + case isPolygon: + this.value = e.detail; + break; + default: + break; } - case isSelection: { - /** @param {import("ol/Feature").default} feature */ - const getProperty = (feature) => - feature.get(this.schema.options.featureProperty) ?? feature; - this.value = e.detail.length - ? e.detail.map(getProperty) - : getProperty(e.detail); - break; - } - case isBox: { - /** @param {import("ol/Feature").default} feature */ - const getExtent = (feature) => feature.getGeometry().getExtent(); - this.value = e.detail?.length - ? e.detail.map(getExtent) - : getExtent(e.detail); - break; - } - case isPolygon: - this.value = e.detail; - break; - default: - break; - } - this.onChange(true); - }, + this.onChange(true); + } + ), ); this.container.appendChild(this.control); diff --git a/elements/jsonform/test/fixtures/collectionSchema.json b/elements/jsonform/test/fixtures/collectionSchema.json index 287c83498..824938ab1 100644 --- a/elements/jsonform/test/fixtures/collectionSchema.json +++ b/elements/jsonform/test/fixtures/collectionSchema.json @@ -627,14 +627,14 @@ "spatial": { "title": "Spatial Extents", "type": "object", - "format": "bounding-boxes", + "format": "bboxes", "required": ["bbox"], "properties": { "bbox": { "type": "array", "items": { "type": "array", - "format": "bounding-box", + "format": "bbox", "items": { "type": "number" }, "minItems": 4, "maxItems": 4 @@ -724,14 +724,14 @@ "spatial": { "title": "Spatial Extents", "type": "object", - "format": "bounding-boxes", + "format": "bboxes", "required": ["bbox"], "properties": { "bbox": { "type": "array", "items": { "type": "array", - "format": "bounding-box", + "format": "bbox", "items": { "type": "number" }, "minItems": 4, "maxItems": 4 From 2614e1c1233494bbc69316cfa2d58b5e0b33da5f Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Fri, 25 Oct 2024 09:40:24 +0200 Subject: [PATCH 8/9] fix: spread features --- .../jsonform/src/custom-inputs/spatial.js | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial.js b/elements/jsonform/src/custom-inputs/spatial.js index b338daa06..0ce4707d3 100644 --- a/elements/jsonform/src/custom-inputs/spatial.js +++ b/elements/jsonform/src/custom-inputs/spatial.js @@ -117,6 +117,25 @@ export class SpatialEditor extends AbstractEditor { this.input.disabled = true; } + const featureProperty = this.schema?.options?.featureProperty; + + /** + * Ensures that features of length 1 are not returned as an array + * + * @param {import("ol/Feature").default|import("ol/Feature").default[]} features + * @param {(feature:import("ol/Feature").default)=>any} callback + */ + const spreadFeatures = (features, callback) => { + if (features.length) { + if (features.length === 1) { + return callback(features[0]); + } + return features.map(callback); + } else { + return callback(features); + } + }; + // Add event listener for change events on the draw tools this.input.addEventListener( "drawupdate", @@ -136,22 +155,21 @@ export class SpatialEditor extends AbstractEditor { case isSelection: { /** @param {import("ol/Feature").default} feature */ const getProperty = (feature) => - feature.get(this.schema.options.featureProperty) ?? feature; - this.value = e.detail.length - ? e.detail.map(getProperty) - : getProperty(e.detail); + featureProperty + ? (feature.get(featureProperty) ?? feature) + : feature; + + this.value = spreadFeatures(e.detail, getProperty); break; } case isBox: { /** @param {import("ol/Feature").default} feature */ const getExtent = (feature) => feature.getGeometry().getExtent(); - this.value = e.detail?.length - ? e.detail.map(getExtent) - : getExtent(e.detail); + this.value = spreadFeatures(e.detail, getExtent); break; } case isPolygon: - this.value = e.detail; + this.value = spreadFeatures(e.detail, (feature) => feature); break; default: break; From 029e3fad62ab95540e1a04c8d7d96bfcb466064d Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Thu, 31 Oct 2024 19:59:20 +0100 Subject: [PATCH 9/9] feat: type spatial custom validator --- elements/jsonform/src/custom-inputs/index.js | 25 +-- .../{spatial.js => spatial/editor.js} | 44 +---- .../src/custom-inputs/spatial/index.js | 2 + .../src/custom-inputs/spatial/utils.js | 75 +++++++ .../src/custom-inputs/spatial/validator.js | 185 ++++++++++++++++++ .../stories/public/boundingBoxSchema.json | 8 +- .../stories/public/featureSchema.json | 5 +- .../stories/public/polygonSchema.json | 8 +- 8 files changed, 296 insertions(+), 56 deletions(-) rename elements/jsonform/src/custom-inputs/{spatial.js => spatial/editor.js} (82%) create mode 100644 elements/jsonform/src/custom-inputs/spatial/index.js create mode 100644 elements/jsonform/src/custom-inputs/spatial/utils.js create mode 100644 elements/jsonform/src/custom-inputs/spatial/validator.js diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index faf66ab2c..fda2f2b01 100644 --- a/elements/jsonform/src/custom-inputs/index.js +++ b/elements/jsonform/src/custom-inputs/index.js @@ -1,6 +1,6 @@ import { JSONEditor } from "@json-editor/json-editor/src/core.js"; import { MinMaxEditor } from "./minmax"; -import { SpatialEditor } from "./spatial"; +import { SpatialEditor, spatialValidator } from "./spatial"; // Define custom input types const inputs = [ @@ -10,52 +10,52 @@ const inputs = [ func: MinMaxEditor, }, { - type: "object", + type: "spatial", format: "bounding-boxes", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "bounding-box", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "bounding-boxes-editor", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "bounding-box-editor", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "polygons", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "polygon", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "polygons-editor", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "polygon-editor", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "feature", func: SpatialEditor, }, { - type: "object", + type: "spatial", format: "features", func: SpatialEditor, }, @@ -67,6 +67,9 @@ const inputs = [ * @param {{[key: string]: any}} startVals - Initial values for the custom inputs */ export const addCustomInputs = (startVals) => { + // Add custom validators for spatial inputs + JSONEditor.defaults["custom_validators"].push(spatialValidator); + // Iterate over each custom input definition inputs.map(({ type, format, func }) => { JSONEditor.defaults["startVals"] = startVals; diff --git a/elements/jsonform/src/custom-inputs/spatial.js b/elements/jsonform/src/custom-inputs/spatial/editor.js similarity index 82% rename from elements/jsonform/src/custom-inputs/spatial.js rename to elements/jsonform/src/custom-inputs/spatial/editor.js index 0ce4707d3..87fc9c33d 100644 --- a/elements/jsonform/src/custom-inputs/spatial.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -1,18 +1,7 @@ import { AbstractEditor } from "@json-editor/json-editor/src/editor.js"; +import { isBox, isMulti, isPolygon, isSelection, setAttributes } from "./utils"; // import "@eox/drawtools"; -/** - * Set multiple attributes to an element - * - * @param {Element} element - The DOM element to set attributes on - * @param {{[key: string]: any}} attributes - The attributes to set on the element - */ -function setAttributes(element, attributes) { - Object.keys(attributes).forEach((attr) => { - element.setAttribute(attr, attributes[attr]); - }); -} - // Define a custom editor class extending AbstractEditor export class SpatialEditor extends AbstractEditor { register() { @@ -48,31 +37,16 @@ export class SpatialEditor extends AbstractEditor { const drawtoolsEl = document.createElement("eox-drawtools"); - const isSelection = ["feature", "features"].some((f) => - this.schema.format.includes(f), - ); - - const isPolygon = ["polygon", "polygons"].some((p) => - this.schema.format.includes(p), - ); - - const isBox = ["bounding-boxes", "bounding-box"].some((p) => - this.schema.format.includes(p), - ); - - const isMulti = ["bounding-boxes", "polygons", "features"].some((m) => - this.schema.format.includes(m), - ); const enableEditor = this.schema.format.includes("editor"); - const drawType = isPolygon ? "Polygon" : "Box"; + const drawType = isPolygon(this.schema) ? "Polygon" : "Box"; const attributes = { type: drawType, }; - if (isSelection) { + if (isSelection(this.schema)) { attributes["layer-id"] = this.schema.options.layerId; } - if (isMulti) { + if (isMulti(this.schema)) { attributes["multiple-features"] = true; } @@ -81,7 +55,7 @@ export class SpatialEditor extends AbstractEditor { attributes["show-editor"] = true; } - if (isMulti) { + if (isMulti(this.schema)) { attributes["show-list"] = true; } @@ -127,7 +101,7 @@ export class SpatialEditor extends AbstractEditor { */ const spreadFeatures = (features, callback) => { if (features.length) { - if (features.length === 1) { + if (!isMulti(this.schema) && features.length === 1) { return callback(features[0]); } return features.map(callback); @@ -152,7 +126,7 @@ export class SpatialEditor extends AbstractEditor { this.value = null; break; } - case isSelection: { + case isSelection(this.schema): { /** @param {import("ol/Feature").default} feature */ const getProperty = (feature) => featureProperty @@ -162,13 +136,13 @@ export class SpatialEditor extends AbstractEditor { this.value = spreadFeatures(e.detail, getProperty); break; } - case isBox: { + case isBox(this.schema): { /** @param {import("ol/Feature").default} feature */ const getExtent = (feature) => feature.getGeometry().getExtent(); this.value = spreadFeatures(e.detail, getExtent); break; } - case isPolygon: + case isPolygon(this.schema): this.value = spreadFeatures(e.detail, (feature) => feature); break; default: diff --git a/elements/jsonform/src/custom-inputs/spatial/index.js b/elements/jsonform/src/custom-inputs/spatial/index.js new file mode 100644 index 000000000..b13926794 --- /dev/null +++ b/elements/jsonform/src/custom-inputs/spatial/index.js @@ -0,0 +1,2 @@ +export { SpatialEditor } from "./editor"; +export { default as spatialValidator } from "./validator"; diff --git a/elements/jsonform/src/custom-inputs/spatial/utils.js b/elements/jsonform/src/custom-inputs/spatial/utils.js new file mode 100644 index 000000000..aef3bd6e4 --- /dev/null +++ b/elements/jsonform/src/custom-inputs/spatial/utils.js @@ -0,0 +1,75 @@ +/** + * Whether a schema has feature/feature format or not + */ +export const isSelection = (schema) => + ["feature", "features"].some((f) => schema?.format?.includes(f)); + +/** + * Whether a schema has ploygon/polygons format or not + */ +export const isPolygon = (schema) => + ["polygon", "polygons"].some((p) => schema?.format?.includes(p)); + +/** + * Whether a schema has bbox/bboxes format or not + */ +export const isBox = (schema) => + ["bounding-boxes", "bounding-box"].some((p) => schema?.format?.includes(p)); + +/** + * Whether a schema expects multiple values not + */ +export const isMulti = (schema) => + ["bounding-boxes", "polygons", "features"].some((m) => + schema?.format?.includes(m), + ); + +/** + * Whether a schema is supported by the spatial editor + **/ +export const isSupported = (schema) => + isSelection(schema) || isPolygon(schema) || isBox(schema); + +/** + * Set multiple attributes to an element + * + * @param {Element} element - The DOM element to set attributes on + * @param {{[key: string]: any}} attributes - The attributes to set on the element + */ +export function setAttributes(element, attributes) { + Object.keys(attributes).forEach((attr) => { + element.setAttribute(attr, attributes[attr]); + }); +} + +/** + * Check if a value satisfies a given type + * supported types: "string", "number", "boolean", "array", "object" + * + * @param {*} val + * @param {string} type + * @returns {boolean} + */ +export const satisfiesType = (val, type) => { + if (!val || !type) { + return false; + } + + switch (type) { + case "string": + return typeof val === "string"; + + case "number": + return !isNaN(val); + + case "boolean": + return typeof val === "boolean"; + + case "array": + return Array.isArray(val); + + case "object": + return typeof val === "object" && !!Object.keys(val).length; + } + return false; +}; diff --git a/elements/jsonform/src/custom-inputs/spatial/validator.js b/elements/jsonform/src/custom-inputs/spatial/validator.js new file mode 100644 index 000000000..5be7b1434 --- /dev/null +++ b/elements/jsonform/src/custom-inputs/spatial/validator.js @@ -0,0 +1,185 @@ +import { + isBox, + isMulti, + isPolygon, + isSelection, + isSupported, + satisfiesType, +} from "./utils"; + +/** + * Validates values of supported spatial types and formats + * + * @param {*} schema + * @param {*} value + * @param {*} path + * @returns {{}} + */ +function spatialValidator(schema, value, path) { + let errors = []; + if (!schema.properties) { + return errors; + } + + Object.keys(schema.properties).forEach((key) => { + const subSchema = schema.properties[key]; + if (subSchema.type !== "spatial" || !isSupported(subSchema)) { + // only validate spatial types and defined formats + return; + } + + const undefinedError = undefinedValidator(key, value[key], path); + if (undefinedError.length) { + errors.push(...undefinedError); + return; + } + + switch (true) { + case isSelection(subSchema): { + errors.push( + ...handleMultiValidation({ + key, + subValue: value[key], + subSchema, + path, + validationFn: selectValidator, + }), + ); + break; + } + case isBox(subSchema): { + errors.push( + ...handleMultiValidation({ + key, + subValue: value[key], + subSchema, + path, + validationFn: bBoxValidator, + }), + ); + break; + } + case isPolygon(subSchema): { + errors.push( + ...handleMultiValidation({ + key, + subValue: value[key], + subSchema, + path, + validationFn: polygonValidator, + }), + ); + break; + } + default: + break; + } + }); + return errors; +} + +export default spatialValidator; +/** + * Handles validating array values of type spatial + */ +function handleMultiValidation({ + key, + subValue, + path, + subSchema, + validationFn, +}) { + if (isMulti(subSchema)) { + if (!Array.isArray(subValue)) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to be an array but got typeof ${typeof subValue}`, + property: "format", + }, + ]; + } else { + return subValue.flatMap((v, i) => + validationFn(`${key}.${i}`, v, path, subSchema), + ); + } + } else { + return validationFn(key, subValue, path, subSchema); + } +} + +/** + * Bounding box validator + */ +function bBoxValidator(key, val, path) { + // expect to return the spacial extent + const errors = []; + if (val.length !== 4) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have 4 values but got ${val.length}`, + property: "format", + }, + ]; + } + + val.forEach((v, i) => { + if (typeof v !== "number") { + errors.push({ + path: `${path}.${key}.${i}`, + message: `extent is expected to be of type number but got ${v}`, + property: "format", + }); + } + }); + return errors; +} + +/** + * Feature selection validator + */ +function selectValidator(key, val, path, subSchema) { + // type can be "string","number","boolean","object","array" + const expected = subSchema.options?.type; + if (expected) { + if (satisfiesType(val, expected)) { + return []; + } else { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to be of type ${expected} but got typeof ${typeof val}`, + property: "format", + }, + ]; + } + } + return []; +} + +function polygonValidator(key, val, path) { + if (typeof val !== "object" && !Object.keys(val).length) { + return [ + { + path: `${path}.${key}`, + message: `Value was expected to be a feature object `, + property: "format", + }, + ]; + } + return []; +} + +function undefinedValidator(key, val, path) { + if (!val) { + return [ + { + path: `${path}.${key}`, + message: `Value is undefined`, + property: "type", + }, + ]; + } + return []; +} diff --git a/elements/jsonform/stories/public/boundingBoxSchema.json b/elements/jsonform/stories/public/boundingBoxSchema.json index 342c75b82..c8b817c16 100644 --- a/elements/jsonform/stories/public/boundingBoxSchema.json +++ b/elements/jsonform/stories/public/boundingBoxSchema.json @@ -3,25 +3,25 @@ "properties": { "bboxes": { "title": "Multi bbox example", - "type": "object", + "type": "spatial", "properties": {}, "format": "bounding-boxes" }, "bbox": { "title": "Single bbox example", - "type": "object", + "type": "spatial", "properties": {}, "format": "bounding-box" }, "bboxes-editor": { "title": "Multi bbox example + Editor", - "type": "object", + "type": "spatial", "properties": {}, "format": "bounding-boxes-editor" }, "bbox-editor": { "title": "Single bbox example + Editor", - "type": "object", + "type": "spatial", "properties": {}, "format": "bounding-box-editor" } diff --git a/elements/jsonform/stories/public/featureSchema.json b/elements/jsonform/stories/public/featureSchema.json index 11f6ba4d1..041a684fd 100644 --- a/elements/jsonform/stories/public/featureSchema.json +++ b/elements/jsonform/stories/public/featureSchema.json @@ -3,17 +3,18 @@ "properties": { "features": { "title": "Multi Features", - "type": "object", + "type": "spatial", "options": { "layerId": "regions-grey", "featureProperty": "BIOME_NAME", + "type": "string", "for": "eox-map#first" }, "format": "features" }, "feature": { "title": "Feature", - "type": "object", + "type": "spatial", "options": { "layerId": "regions-blue", "for": "eox-map#second" diff --git a/elements/jsonform/stories/public/polygonSchema.json b/elements/jsonform/stories/public/polygonSchema.json index 3f4cf005b..bf00fb27b 100644 --- a/elements/jsonform/stories/public/polygonSchema.json +++ b/elements/jsonform/stories/public/polygonSchema.json @@ -3,25 +3,25 @@ "properties": { "polygons": { "title": "Multi polygon", - "type": "object", + "type": "spatial", "properties": {}, "format": "polygons" }, "polygon": { "title": "Single polygon", - "type": "object", + "type": "spatial", "properties": {}, "format": "polygon" }, "polygons-editor": { "title": "Multi polygon + Editor", - "type": "object", + "type": "spatial", "properties": {}, "format": "polygons-editor" }, "polygon-editor": { "title": "Single polygon + Editor", - "type": "object", + "type": "spatial", "properties": {}, "format": "polygon-editor" }