From 180306b38c116328bd2f1be50ab01ea374803bef Mon Sep 17 00:00:00 2001 From: santilland Date: Wed, 25 Sep 2024 16:45:17 +0200 Subject: [PATCH 01/24] 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 02/24] 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 03/24] 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 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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" } From 5b6afce1feb7432009be13f50cd004aaf622387e Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Thu, 7 Nov 2024 12:18:48 +0100 Subject: [PATCH 10/24] chore: update collection schema --- .../jsonform/stories/public/collectionSchema.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/elements/jsonform/stories/public/collectionSchema.json b/elements/jsonform/stories/public/collectionSchema.json index 287c83498..cafa001ce 100644 --- a/elements/jsonform/stories/public/collectionSchema.json +++ b/elements/jsonform/stories/public/collectionSchema.json @@ -631,15 +631,8 @@ "required": ["bbox"], "properties": { "bbox": { - "type": "array", - "items": { - "type": "array", - "format": "bounding-box", - "items": { "type": "number" }, - "minItems": 4, - "maxItems": 4 - }, - "minItems": 1 + "type": "spatial", + "format": "bounding-boxes" } } }, From c706f7f0f7fd0a116dc59a954600214762b9e128 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Thu, 7 Nov 2024 12:24:06 +0100 Subject: [PATCH 11/24] test: render drawtools on type spatial --- .../stories/public/collectionSchema.json | 2 +- elements/jsonform/test/_mockedDrawtools.js | 21 +++++++++ elements/jsonform/test/cases/index.js | 1 + .../jsonform/test/cases/render-drawtools.js | 47 +++++++++++++++++++ .../jsonform/test/fixtures/spatialSchema.json | 20 ++++++++ elements/jsonform/test/general.cy.js | 3 ++ 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 elements/jsonform/test/_mockedDrawtools.js create mode 100644 elements/jsonform/test/cases/render-drawtools.js create mode 100644 elements/jsonform/test/fixtures/spatialSchema.json diff --git a/elements/jsonform/stories/public/collectionSchema.json b/elements/jsonform/stories/public/collectionSchema.json index cafa001ce..692ea7602 100644 --- a/elements/jsonform/stories/public/collectionSchema.json +++ b/elements/jsonform/stories/public/collectionSchema.json @@ -632,7 +632,7 @@ "properties": { "bbox": { "type": "spatial", - "format": "bounding-boxes" + "format": "bounding-boxes" } } }, diff --git a/elements/jsonform/test/_mockedDrawtools.js b/elements/jsonform/test/_mockedDrawtools.js new file mode 100644 index 000000000..1cf81667b --- /dev/null +++ b/elements/jsonform/test/_mockedDrawtools.js @@ -0,0 +1,21 @@ +class MockedDrawTools extends HTMLElement { + static observedAttributes = [ + "for", + "layer-id", + "multiple-features", + "show-editor", + "show-list", + "type", + ]; + constructor() { + super(); + this.for = ""; + this["layer-id"] = ""; + this["multiple-features"] = false; + this["show-editor"] = false; + this["show-list"] = false; + this.type = ""; + } +} + +customElements.define("eox-drawtools", MockedDrawTools); diff --git a/elements/jsonform/test/cases/index.js b/elements/jsonform/test/cases/index.js index 33b01773f..b3977eb90 100644 --- a/elements/jsonform/test/cases/index.js +++ b/elements/jsonform/test/cases/index.js @@ -9,3 +9,4 @@ export { default as loadMarkdownTest } from "./load-markdown"; export { default as triggerChangeEventTest } from "./trigger-change-event"; export { default as loadValuesTest } from "./load-values"; export { default as loadMisMatchingValuesTest } from "./load-mismatching-values"; +export { default as renderDrawtools } from "./render-drawtools"; diff --git a/elements/jsonform/test/cases/render-drawtools.js b/elements/jsonform/test/cases/render-drawtools.js new file mode 100644 index 000000000..596de22ef --- /dev/null +++ b/elements/jsonform/test/cases/render-drawtools.js @@ -0,0 +1,47 @@ +import { html } from "lit"; +import { TEST_SELECTORS } from "../../src/enums"; +import schemaFixture from "../fixtures/spatialSchema.json"; +// Destructure TEST_SELECTORS object +const { jsonForm } = TEST_SELECTORS; + +const checkDrawtoolsForTypeSpatial = () => { + cy.intercept("**/spatialSchema.json", (req) => { + req.reply(schemaFixture); + }); + + cy.mount( + html` + `, + ).as(jsonForm); + cy.get(jsonForm) + .shadow() + .within(() => { + cy.get('eox-drawtools[id="root[bbox]"]').then(($el) => { + // Check if the drawtools are rendered + expect($el[0]).to.exist; + // Check if the drawtools have the correct `for` attribute + expect($el[0].getAttribute("for")).to.equal( + schemaFixture.properties.bbox.options.for, + ); + // Check if the drawtools have the correct `type` attribute + expect($el[0].getAttribute("type")).to.equal("Box"); + }); + + cy.get('eox-drawtools[id="root[polygons]"]').then(($el) => { + // check if the drawtools are rendered + expect($el[0]).to.exist; + // check if the drawtools have the correct `type` attribute + expect($el[0].getAttribute("type")).to.equal("Polygon"); + // check if the drawtools have the correct `for` attribute + expect($el[0].getAttribute("for")).to.equal( + schemaFixture.properties.polygons.options.for, + ); + // check if the drawtools `multiple-features` + // and `show-list` attributes are set in case of a plural format + expect($el[0].getAttribute("multiple-features")).to.equal("true"); + expect($el[0].getAttribute("show-list")).to.equal("true"); + }); + }); +}; + +export default checkDrawtoolsForTypeSpatial; diff --git a/elements/jsonform/test/fixtures/spatialSchema.json b/elements/jsonform/test/fixtures/spatialSchema.json new file mode 100644 index 000000000..0c4c27161 --- /dev/null +++ b/elements/jsonform/test/fixtures/spatialSchema.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "bbox": { + "type": "spatial", + "options": { + "for": "mocked-map#bbox" + }, + "format": "bounding-box" + }, + "polygons": { + "type": "spatial", + "options": { + "for": "mocked-map#polygons" + }, + "format": "polygons" + } + } + } + \ No newline at end of file diff --git a/elements/jsonform/test/general.cy.js b/elements/jsonform/test/general.cy.js index 64f4fe7fb..4b986e0d2 100644 --- a/elements/jsonform/test/general.cy.js +++ b/elements/jsonform/test/general.cy.js @@ -1,5 +1,6 @@ // Importing necessary modules, test cases, and enums import "../src/main"; +import "./_mockedDrawtools"; import { loadJsonFormTest, loadJsonFormNoShadowTest, @@ -10,6 +11,7 @@ import { triggerChangeEventTest, loadValuesTest, loadMisMatchingValuesTest, + renderDrawtools, } from "./cases"; // Test suite for Jsonform @@ -25,4 +27,5 @@ describe("Jsonform", () => { it("triggers a change event when typing", () => triggerChangeEventTest()); it("loads values", () => loadValuesTest()); it("loads mismatching values", () => loadMisMatchingValuesTest()); + it("renders drawtools on type spatial", () => renderDrawtools()); }); From cf8599b927679818ebd5fca2bb4486745528876c Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Thu, 7 Nov 2024 13:35:13 +0100 Subject: [PATCH 12/24] feat: add projection option --- elements/jsonform/src/custom-inputs/spatial/editor.js | 5 +++++ elements/jsonform/test/cases/render-drawtools.js | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index 87fc9c33d..ac9bb41d0 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -43,6 +43,11 @@ export class SpatialEditor extends AbstractEditor { const attributes = { type: drawType, }; + + if (this.schema?.options?.projection) { + attributes.projection = this.schema.options.projection; + } + if (isSelection(this.schema)) { attributes["layer-id"] = this.schema.options.layerId; } diff --git a/elements/jsonform/test/cases/render-drawtools.js b/elements/jsonform/test/cases/render-drawtools.js index 596de22ef..22c3ef4ac 100644 --- a/elements/jsonform/test/cases/render-drawtools.js +++ b/elements/jsonform/test/cases/render-drawtools.js @@ -10,8 +10,7 @@ const checkDrawtoolsForTypeSpatial = () => { }); cy.mount( - html` - `, + html` `, ).as(jsonForm); cy.get(jsonForm) .shadow() From 89852560ae6a5bc594adb80b7e5123a293cde3c6 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Thu, 7 Nov 2024 13:56:06 +0100 Subject: [PATCH 13/24] test: projection --- elements/jsonform/test/_mockedDrawtools.js | 2 ++ elements/jsonform/test/cases/render-drawtools.js | 4 ++++ elements/jsonform/test/fixtures/spatialSchema.json | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/elements/jsonform/test/_mockedDrawtools.js b/elements/jsonform/test/_mockedDrawtools.js index 1cf81667b..0e54a0c75 100644 --- a/elements/jsonform/test/_mockedDrawtools.js +++ b/elements/jsonform/test/_mockedDrawtools.js @@ -6,6 +6,7 @@ class MockedDrawTools extends HTMLElement { "show-editor", "show-list", "type", + "projection", ]; constructor() { super(); @@ -14,6 +15,7 @@ class MockedDrawTools extends HTMLElement { this["multiple-features"] = false; this["show-editor"] = false; this["show-list"] = false; + this.projection = "EPSG:4326"; this.type = ""; } } diff --git a/elements/jsonform/test/cases/render-drawtools.js b/elements/jsonform/test/cases/render-drawtools.js index 22c3ef4ac..f1ed81035 100644 --- a/elements/jsonform/test/cases/render-drawtools.js +++ b/elements/jsonform/test/cases/render-drawtools.js @@ -24,6 +24,10 @@ const checkDrawtoolsForTypeSpatial = () => { ); // Check if the drawtools have the correct `type` attribute expect($el[0].getAttribute("type")).to.equal("Box"); + // Check if the drawtools have the correct `projection` attribute + expect($el[0].getAttribute("projection")).to.equal( + schemaFixture.properties.bbox.options.projection, + ); }); cy.get('eox-drawtools[id="root[polygons]"]').then(($el) => { diff --git a/elements/jsonform/test/fixtures/spatialSchema.json b/elements/jsonform/test/fixtures/spatialSchema.json index 0c4c27161..0324189fe 100644 --- a/elements/jsonform/test/fixtures/spatialSchema.json +++ b/elements/jsonform/test/fixtures/spatialSchema.json @@ -4,7 +4,8 @@ "bbox": { "type": "spatial", "options": { - "for": "mocked-map#bbox" + "for": "mocked-map#bbox", + "projection": "EPSG:3857" }, "format": "bounding-box" }, From 1d9f22afc5188f93160f81f7e0890d6d079d3bab Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Fri, 22 Nov 2024 09:41:27 +0100 Subject: [PATCH 14/24] fix: auto start drawing --- .../jsonform/src/custom-inputs/spatial/editor.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index ac9bb41d0..0af748fe6 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -14,11 +14,9 @@ export class SpatialEditor extends AbstractEditor { // 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) @@ -35,7 +33,9 @@ export class SpatialEditor extends AbstractEditor { this.translateProperty(options.infoText), ); - const drawtoolsEl = document.createElement("eox-drawtools"); + const drawtoolsEl = /** @type {import("@eox/drawtools").EOxDrawTools} */ ( + document.createElement("eox-drawtools") + ); const enableEditor = this.schema.format.includes("editor"); @@ -81,6 +81,12 @@ export class SpatialEditor extends AbstractEditor { attributes.for = "eox-map#" + mapId; } setAttributes(drawtoolsEl, attributes); + const autoDraw = !(options.autoStartSelection === false) + if (autoDraw) { + drawtoolsEl.updateComplete.then(() => { + drawtoolsEl.startDrawing(); + }); + } this.input = drawtoolsEl; this.input.id = this.formname; From f7902b11466d60031f4cc4c60c9c6c977dba5f9f Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Fri, 22 Nov 2024 09:55:03 +0100 Subject: [PATCH 15/24] fix: remove `*-editor` formats --- elements/jsonform/src/custom-inputs/index.js | 20 ------------------- .../src/custom-inputs/spatial/editor.js | 7 ------- .../stories/public/boundingBoxSchema.json | 14 +++++-------- .../stories/public/polygonSchema.json | 12 ----------- 4 files changed, 5 insertions(+), 48 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index fda2f2b01..0dc022f1d 100644 --- a/elements/jsonform/src/custom-inputs/index.js +++ b/elements/jsonform/src/custom-inputs/index.js @@ -19,16 +19,6 @@ const inputs = [ format: "bounding-box", func: SpatialEditor, }, - { - type: "spatial", - format: "bounding-boxes-editor", - func: SpatialEditor, - }, - { - type: "spatial", - format: "bounding-box-editor", - func: SpatialEditor, - }, { type: "spatial", format: "polygons", @@ -39,16 +29,6 @@ const inputs = [ format: "polygon", func: SpatialEditor, }, - { - type: "spatial", - format: "polygons-editor", - func: SpatialEditor, - }, - { - type: "spatial", - format: "polygon-editor", - func: SpatialEditor, - }, { type: "spatial", format: "feature", diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index 0af748fe6..fb6e64dee 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -37,8 +37,6 @@ export class SpatialEditor extends AbstractEditor { document.createElement("eox-drawtools") ); - const enableEditor = this.schema.format.includes("editor"); - const drawType = isPolygon(this.schema) ? "Polygon" : "Box"; const attributes = { type: drawType, @@ -55,11 +53,6 @@ export class SpatialEditor extends AbstractEditor { attributes["multiple-features"] = true; } - if (enableEditor) { - attributes["import-features"] = true; - attributes["show-editor"] = true; - } - if (isMulti(this.schema)) { attributes["show-list"] = true; } diff --git a/elements/jsonform/stories/public/boundingBoxSchema.json b/elements/jsonform/stories/public/boundingBoxSchema.json index c8b817c16..48b421415 100644 --- a/elements/jsonform/stories/public/boundingBoxSchema.json +++ b/elements/jsonform/stories/public/boundingBoxSchema.json @@ -14,16 +14,12 @@ "format": "bounding-box" }, "bboxes-editor": { - "title": "Multi bbox example + Editor", + "title": "Multi bbox example without auto start drawing", "type": "spatial", - "properties": {}, - "format": "bounding-boxes-editor" - }, - "bbox-editor": { - "title": "Single bbox example + Editor", - "type": "spatial", - "properties": {}, - "format": "bounding-box-editor" + "options":{ + "autoStartSelection":false + }, + "format": "bounding-boxes" } } } diff --git a/elements/jsonform/stories/public/polygonSchema.json b/elements/jsonform/stories/public/polygonSchema.json index bf00fb27b..c47a201c5 100644 --- a/elements/jsonform/stories/public/polygonSchema.json +++ b/elements/jsonform/stories/public/polygonSchema.json @@ -12,18 +12,6 @@ "type": "spatial", "properties": {}, "format": "polygon" - }, - "polygons-editor": { - "title": "Multi polygon + Editor", - "type": "spatial", - "properties": {}, - "format": "polygons-editor" - }, - "polygon-editor": { - "title": "Single polygon + Editor", - "type": "spatial", - "properties": {}, - "format": "polygon-editor" } } } From ddf34590ab73cd3b5e602e4b7ac684c1a2eb7089 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Fri, 22 Nov 2024 13:47:54 +0100 Subject: [PATCH 16/24] fix: update editors types & adapt the validator and stories --- elements/jsonform/src/custom-inputs/index.js | 19 +- .../src/custom-inputs/spatial/editor.js | 8 +- .../src/custom-inputs/spatial/index.js | 2 +- .../src/custom-inputs/spatial/utils.js | 10 +- .../src/custom-inputs/spatial/validator.js | 176 ++++++++++-------- .../stories/public/boundingBoxSchema.json | 12 +- .../stories/public/collectionSchema.json | 2 +- .../stories/public/featureSchema.json | 11 +- .../stories/public/polygonSchema.json | 4 +- 9 files changed, 137 insertions(+), 107 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index 0dc022f1d..23e42cf7d 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, spatialValidator } from "./spatial"; +import { SpatialEditor, spatialValidatorCreator } from "./spatial"; // Define custom input types const inputs = [ @@ -10,32 +10,31 @@ const inputs = [ func: MinMaxEditor, }, { - type: "spatial", + type: "array", format: "bounding-boxes", func: SpatialEditor, }, { - type: "spatial", + type: "array", format: "bounding-box", func: SpatialEditor, }, { - type: "spatial", + type: "array", format: "polygons", func: SpatialEditor, }, { - type: "spatial", + type: "object", format: "polygon", func: SpatialEditor, }, { - type: "spatial", format: "feature", func: SpatialEditor, }, { - type: "spatial", + type: "array", format: "features", func: SpatialEditor, }, @@ -48,7 +47,9 @@ const inputs = [ */ export const addCustomInputs = (startVals) => { // Add custom validators for spatial inputs - JSONEditor.defaults["custom_validators"].push(spatialValidator); + JSONEditor.defaults["custom_validators"].push( + spatialValidatorCreator(inputs), + ); // Iterate over each custom input definition inputs.map(({ type, format, func }) => { @@ -58,6 +59,8 @@ export const addCustomInputs = (startVals) => { // Add a resolver to determine which format to use based on the schema JSONEditor.defaults.resolvers.unshift((schema) => { if (schema.type === type && schema.format === format) return format; + // If the schema format is "feature" use the SpatialEditor for all types + if (schema.format === "feature") return format; }); }); }; diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index fb6e64dee..e670e90c9 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -74,8 +74,8 @@ export class SpatialEditor extends AbstractEditor { attributes.for = "eox-map#" + mapId; } setAttributes(drawtoolsEl, attributes); - const autoDraw = !(options.autoStartSelection === false) - if (autoDraw) { + const autoDraw = !(options.autoStartSelection === false); + if (autoDraw) { drawtoolsEl.updateComplete.then(() => { drawtoolsEl.startDrawing(); }); @@ -101,7 +101,7 @@ export class SpatialEditor extends AbstractEditor { * 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 + * @param {(feature:import("ol/Feature").default) => any} callback */ const spreadFeatures = (features, callback) => { if (features.length) { @@ -126,7 +126,7 @@ export class SpatialEditor extends AbstractEditor { e.stopPropagation(); switch (true) { - case !e.detail || e.detail?.length === 0: { + case !e.detail || !e.detail?.length: { this.value = null; break; } diff --git a/elements/jsonform/src/custom-inputs/spatial/index.js b/elements/jsonform/src/custom-inputs/spatial/index.js index b13926794..5937a58c3 100644 --- a/elements/jsonform/src/custom-inputs/spatial/index.js +++ b/elements/jsonform/src/custom-inputs/spatial/index.js @@ -1,2 +1,2 @@ export { SpatialEditor } from "./editor"; -export { default as spatialValidator } from "./validator"; +export { default as spatialValidatorCreator } from "./validator"; diff --git a/elements/jsonform/src/custom-inputs/spatial/utils.js b/elements/jsonform/src/custom-inputs/spatial/utils.js index aef3bd6e4..a4703b973 100644 --- a/elements/jsonform/src/custom-inputs/spatial/utils.js +++ b/elements/jsonform/src/custom-inputs/spatial/utils.js @@ -2,27 +2,25 @@ * Whether a schema has feature/feature format or not */ export const isSelection = (schema) => - ["feature", "features"].some((f) => schema?.format?.includes(f)); + ["feature", "features"].some((f) => schema?.format === f); /** * Whether a schema has ploygon/polygons format or not */ export const isPolygon = (schema) => - ["polygon", "polygons"].some((p) => schema?.format?.includes(p)); + ["polygon", "polygons"].some((p) => schema?.format === p); /** * Whether a schema has bbox/bboxes format or not */ export const isBox = (schema) => - ["bounding-boxes", "bounding-box"].some((p) => schema?.format?.includes(p)); + ["bounding-boxes", "bounding-box"].some((p) => schema?.format === p); /** * Whether a schema expects multiple values not */ export const isMulti = (schema) => - ["bounding-boxes", "polygons", "features"].some((m) => - schema?.format?.includes(m), - ); + ["bounding-boxes", "polygons", "features"].some((m) => schema?.format === m); /** * Whether a schema is supported by the spatial editor diff --git a/elements/jsonform/src/custom-inputs/spatial/validator.js b/elements/jsonform/src/custom-inputs/spatial/validator.js index 5be7b1434..e71521134 100644 --- a/elements/jsonform/src/custom-inputs/spatial/validator.js +++ b/elements/jsonform/src/custom-inputs/spatial/validator.js @@ -8,77 +8,92 @@ import { } 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; + * @param {{ + * type?: string + * format: string + * func: Record & { new (): any } + * }[]} inputs + **/ +function spatialValidatorCreator(inputs) { + /** + * Validates values of supported spatial types and formats + * + * @param {*} schema + * @param {*} value + * @param {*} path + * @returns {{}} + */ + return function (schema, value, path) { + let errors = []; + if (!schema.properties) { + return errors; } - const undefinedError = undefinedValidator(key, value[key], path); - if (undefinedError.length) { - errors.push(...undefinedError); - return; - } + Object.keys(schema.properties).forEach((key) => { + const subSchema = schema.properties[key]; + const toBeValidated = + isSupported(subSchema) && + (subSchema.format === "feature" || + inputs.some( + (i) => i.format === subSchema.format && i.type === subSchema.type, + )); - switch (true) { - case isSelection(subSchema): { - errors.push( - ...handleMultiValidation({ - key, - subValue: value[key], - subSchema, - path, - validationFn: selectValidator, - }), - ); - break; + if (!toBeValidated) { + // only validate defined types and formats using the spatial editor + return; } - case isBox(subSchema): { - errors.push( - ...handleMultiValidation({ - key, - subValue: value[key], - subSchema, - path, - validationFn: bBoxValidator, - }), - ); - break; + + const undefinedError = undefinedValidator(key, value[key], path); + if (undefinedError.length) { + errors.push(...undefinedError); + return; } - case isPolygon(subSchema): { - errors.push( - ...handleMultiValidation({ - key, - subValue: value[key], - subSchema, - path, - validationFn: polygonValidator, - }), - ); - break; + + 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; } - default: - break; - } - }); - return errors; + }); + return errors; + }; } - -export default spatialValidator; +export default spatialValidatorCreator; /** * Handles validating array values of type spatial */ @@ -98,11 +113,19 @@ function handleMultiValidation({ property: "format", }, ]; - } else { - return subValue.flatMap((v, i) => - validationFn(`${key}.${i}`, v, path, subSchema), - ); + } else if (!subValue.length) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have at least one value`, + property: "format", + }, + ]; } + + return subValue?.flatMap((v, i) => + validationFn(`${key}.${i}`, v, path, subSchema), + ); } else { return validationFn(key, subValue, path, subSchema); } @@ -140,26 +163,29 @@ function bBoxValidator(key, val, path) { * Feature selection validator */ function selectValidator(key, val, path, subSchema) { - // type can be "string","number","boolean","object","array" - const expected = subSchema.options?.type; + let expected; + if (isMulti(subSchema)) { + expected = subSchema?.items?.type; + } else { + expected = subSchema.type; + } if (expected) { + // type can be "string","number","boolean","object","array" if (satisfiesType(val, expected)) { return []; } else { return [ { path: `${path}.${key}`, - message: `Value is expected to be of type ${expected} but got typeof ${typeof val}`, + message: `Value is expected to be a valid ${expected}`, property: "format", }, ]; } } - return []; } - function polygonValidator(key, val, path) { - if (typeof val !== "object" && !Object.keys(val).length) { + if (typeof val !== "object" || !Object.keys(val).length) { return [ { path: `${path}.${key}`, @@ -176,7 +202,7 @@ function undefinedValidator(key, val, path) { return [ { path: `${path}.${key}`, - message: `Value is undefined`, + message: `invalid value ${JSON.stringify(val)}`, property: "type", }, ]; diff --git a/elements/jsonform/stories/public/boundingBoxSchema.json b/elements/jsonform/stories/public/boundingBoxSchema.json index 48b421415..5c29baba4 100644 --- a/elements/jsonform/stories/public/boundingBoxSchema.json +++ b/elements/jsonform/stories/public/boundingBoxSchema.json @@ -3,21 +3,21 @@ "properties": { "bboxes": { "title": "Multi bbox example", - "type": "spatial", + "type": "array", "properties": {}, "format": "bounding-boxes" }, "bbox": { "title": "Single bbox example", - "type": "spatial", + "type": "array", "properties": {}, "format": "bounding-box" }, - "bboxes-editor": { + "bboxes2": { "title": "Multi bbox example without auto start drawing", - "type": "spatial", - "options":{ - "autoStartSelection":false + "type": "array", + "options": { + "autoStartSelection": false }, "format": "bounding-boxes" } diff --git a/elements/jsonform/stories/public/collectionSchema.json b/elements/jsonform/stories/public/collectionSchema.json index 692ea7602..8d03d6a92 100644 --- a/elements/jsonform/stories/public/collectionSchema.json +++ b/elements/jsonform/stories/public/collectionSchema.json @@ -631,7 +631,7 @@ "required": ["bbox"], "properties": { "bbox": { - "type": "spatial", + "type": "array", "format": "bounding-boxes" } } diff --git a/elements/jsonform/stories/public/featureSchema.json b/elements/jsonform/stories/public/featureSchema.json index 041a684fd..70216d373 100644 --- a/elements/jsonform/stories/public/featureSchema.json +++ b/elements/jsonform/stories/public/featureSchema.json @@ -3,20 +3,23 @@ "properties": { "features": { "title": "Multi Features", - "type": "spatial", + "type": "array", "options": { "layerId": "regions-grey", - "featureProperty": "BIOME_NAME", - "type": "string", "for": "eox-map#first" }, + "items": { + "type": "object" + }, "format": "features" }, "feature": { "title": "Feature", - "type": "spatial", + "type": "string", "options": { "layerId": "regions-blue", + "featureProperty": "BIOME_NAME", + "type": "string", "for": "eox-map#second" }, "format": "feature" diff --git a/elements/jsonform/stories/public/polygonSchema.json b/elements/jsonform/stories/public/polygonSchema.json index c47a201c5..6a93460b9 100644 --- a/elements/jsonform/stories/public/polygonSchema.json +++ b/elements/jsonform/stories/public/polygonSchema.json @@ -3,13 +3,13 @@ "properties": { "polygons": { "title": "Multi polygon", - "type": "spatial", + "type": "array", "properties": {}, "format": "polygons" }, "polygon": { "title": "Single polygon", - "type": "spatial", + "type": "object", "properties": {}, "format": "polygon" } From ac976e7480a9fd84cb5291e1e191f35b9505ec54 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Tue, 26 Nov 2024 16:17:09 +0100 Subject: [PATCH 17/24] feat: support format point/points --- elements/jsonform/src/custom-inputs/index.js | 10 +++++ .../src/custom-inputs/spatial/editor.js | 36 +++++++++++++--- .../src/custom-inputs/spatial/utils.js | 12 +++++- .../src/custom-inputs/spatial/validator.js | 43 ++++++++++++++++++- elements/jsonform/stories/index.js | 1 + elements/jsonform/stories/jsonform.stories.js | 8 +++- elements/jsonform/stories/points.js | 12 ++++++ .../jsonform/stories/public/pointSchema.json | 18 ++++++++ 8 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 elements/jsonform/stories/points.js create mode 100644 elements/jsonform/stories/public/pointSchema.json diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index 23e42cf7d..b0e8ffe8c 100644 --- a/elements/jsonform/src/custom-inputs/index.js +++ b/elements/jsonform/src/custom-inputs/index.js @@ -29,6 +29,16 @@ const inputs = [ format: "polygon", func: SpatialEditor, }, + { + type: "array", + format: "points", + func: SpatialEditor, + }, + { + type: "array", + format: "point", + func: SpatialEditor, + }, { format: "feature", func: SpatialEditor, diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index e670e90c9..1fe271c84 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -1,5 +1,12 @@ import { AbstractEditor } from "@json-editor/json-editor/src/editor.js"; -import { isBox, isMulti, isPolygon, isSelection, setAttributes } from "./utils"; +import { + isBox, + isMulti, + isPoint, + isPolygon, + isSelection, + setAttributes, +} from "./utils"; // import "@eox/drawtools"; // Define a custom editor class extending AbstractEditor @@ -37,7 +44,22 @@ export class SpatialEditor extends AbstractEditor { document.createElement("eox-drawtools") ); - const drawType = isPolygon(this.schema) ? "Polygon" : "Box"; + let drawType; + switch (true) { + case isPolygon(this.schema): + drawType = "Polygon"; + break; + case isBox(this.schema): + drawType = "Box"; + break; + case isPoint(this.schema): + drawType = "Point"; + break; + default: + drawType = "Box"; + break; + } + const attributes = { type: drawType, }; @@ -51,9 +73,6 @@ export class SpatialEditor extends AbstractEditor { } if (isMulti(this.schema)) { attributes["multiple-features"] = true; - } - - if (isMulti(this.schema)) { attributes["show-list"] = true; } @@ -149,6 +168,12 @@ export class SpatialEditor extends AbstractEditor { case isPolygon(this.schema): this.value = spreadFeatures(e.detail, (feature) => feature); break; + case isPoint(this.schema): + this.value = spreadFeatures(e.detail, (feature) => + //@ts-expect-error getCoordinates does not exist on Geometry + feature.getGeometry()?.getCoordinates(), + ); + break; default: break; } @@ -157,7 +182,6 @@ export class SpatialEditor extends AbstractEditor { } ), ); - this.container.appendChild(this.control); } diff --git a/elements/jsonform/src/custom-inputs/spatial/utils.js b/elements/jsonform/src/custom-inputs/spatial/utils.js index a4703b973..887b96090 100644 --- a/elements/jsonform/src/custom-inputs/spatial/utils.js +++ b/elements/jsonform/src/custom-inputs/spatial/utils.js @@ -10,6 +10,12 @@ export const isSelection = (schema) => export const isPolygon = (schema) => ["polygon", "polygons"].some((p) => schema?.format === p); +/** + * Whether a schema has point/points format or not + */ +export const isPoint = (schema) => + ["point", "points"].some((p) => schema?.format === p); + /** * Whether a schema has bbox/bboxes format or not */ @@ -20,13 +26,15 @@ export const isBox = (schema) => * Whether a schema expects multiple values not */ export const isMulti = (schema) => - ["bounding-boxes", "polygons", "features"].some((m) => schema?.format === m); + ["bounding-boxes", "polygons", "features", "points"].some( + (m) => schema?.format === m, + ); /** * Whether a schema is supported by the spatial editor **/ export const isSupported = (schema) => - isSelection(schema) || isPolygon(schema) || isBox(schema); + isSelection(schema) || isPolygon(schema) || isBox(schema) || isPoint(schema); /** * Set multiple attributes to an element diff --git a/elements/jsonform/src/custom-inputs/spatial/validator.js b/elements/jsonform/src/custom-inputs/spatial/validator.js index e71521134..0c328752f 100644 --- a/elements/jsonform/src/custom-inputs/spatial/validator.js +++ b/elements/jsonform/src/custom-inputs/spatial/validator.js @@ -1,6 +1,7 @@ import { isBox, isMulti, + isPoint, isPolygon, isSelection, isSupported, @@ -86,6 +87,18 @@ function spatialValidatorCreator(inputs) { ); break; } + case isPoint(subSchema): { + errors.push( + ...handleMultiValidation({ + key, + subValue: value[key], + subSchema, + path, + validationFn: pointValidator, + }), + ); + break; + } default: break; } @@ -135,13 +148,13 @@ function handleMultiValidation({ * Bounding box validator */ function bBoxValidator(key, val, path) { - // expect to return the spacial extent + // expect to return the spatial extent const errors = []; if (val.length !== 4) { return [ { path: `${path}.${key}`, - message: `Value is expected to have 4 values but got ${val.length}`, + message: `Value is expected to have 4 items but got ${val.length}`, property: "format", }, ]; @@ -183,6 +196,7 @@ function selectValidator(key, val, path, subSchema) { ]; } } + return []; } function polygonValidator(key, val, path) { if (typeof val !== "object" || !Object.keys(val).length) { @@ -197,6 +211,31 @@ function polygonValidator(key, val, path) { return []; } +function pointValidator(key, val, path) { + // expect to return point coordinates + const errors = []; + if (val.length !== 2) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have 2 items but got ${val.length}`, + property: "format", + }, + ]; + } + + val.forEach((v, i) => { + if (typeof v !== "number") { + errors.push({ + path: `${path}.${key}.${i}`, + message: `coordinates is expected to be of type number but got ${v}`, + property: "format", + }); + } + }); + return errors; +} + function undefinedValidator(key, val, path) { if (!val) { return [ diff --git a/elements/jsonform/stories/index.js b/elements/jsonform/stories/index.js index e6bfaa61c..9efa7049d 100644 --- a/elements/jsonform/stories/index.js +++ b/elements/jsonform/stories/index.js @@ -6,4 +6,5 @@ export { default as MarkdownStory } from "./markdown"; // Input form based on Ma export { default as UnStyledStory } from "./unstyled"; // Unstyled input form 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 PointStory } from "./points"; // Input form based on drawtools - Point 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 66ee385f2..16fd5a069 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -6,10 +6,11 @@ import { ExternalStory, MarkdownStory, PrimaryStory, - UnStyledStory, BoundingBoxStory, PolygonStory, FeatureSelectionStory, + PointStory, + UnStyledStory, } from "./index.js"; export default { @@ -61,6 +62,11 @@ export const BoundigBox = BoundingBoxStory; */ export const Polygons = PolygonStory; +/** + * JSON Form based on drawtools - Point + */ +export const Points = PointStory; + /** * JSON Form based on drawtools - Feature Selection */ diff --git a/elements/jsonform/stories/points.js b/elements/jsonform/stories/points.js new file mode 100644 index 000000000..cd818e55f --- /dev/null +++ b/elements/jsonform/stories/points.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 pointSchema from "./public/pointSchema.json"; + +const Point = { + args: { + schema: pointSchema, + }, +}; +export default Point; diff --git a/elements/jsonform/stories/public/pointSchema.json b/elements/jsonform/stories/public/pointSchema.json new file mode 100644 index 000000000..3ff3591cc --- /dev/null +++ b/elements/jsonform/stories/public/pointSchema.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "properties": { + "point": { + "title": "Single point example", + "type": "array", + "format": "point" + }, + "points": { + "title": "Multiple points example", + "type": "array", + "options": { + "projection": "EPSG:3857" + }, + "format": "points" + } + } +} From 4af41c032bdb9f8ca5d2847f6d3732747e8e1f88 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Wed, 27 Nov 2024 14:14:04 +0100 Subject: [PATCH 18/24] feat: support wkt and geojson types and handle their validations --- elements/jsonform/src/custom-inputs/index.js | 70 +++++++++++++++++++ .../src/custom-inputs/spatial/editor.js | 53 ++++++++++++-- .../src/custom-inputs/spatial/utils.js | 10 +++ .../src/custom-inputs/spatial/validator.js | 57 ++++++++++++++- elements/jsonform/stories/geojson.js | 27 +++++++ elements/jsonform/stories/index.js | 2 + elements/jsonform/stories/jsonform.stories.js | 11 +++ elements/jsonform/stories/points.js | 2 +- .../stories/public/geojsonSchema.json | 26 +++++++ .../jsonform/stories/public/wktSchema.json | 26 +++++++ elements/jsonform/stories/wkt.js | 27 +++++++ 11 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 elements/jsonform/stories/geojson.js create mode 100644 elements/jsonform/stories/public/geojsonSchema.json create mode 100644 elements/jsonform/stories/public/wktSchema.json create mode 100644 elements/jsonform/stories/wkt.js diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index b0e8ffe8c..5ce98f78d 100644 --- a/elements/jsonform/src/custom-inputs/index.js +++ b/elements/jsonform/src/custom-inputs/index.js @@ -14,31 +14,91 @@ const inputs = [ format: "bounding-boxes", func: SpatialEditor, }, + { + type: "wkt", + format: "bounding-boxes", + func: SpatialEditor, + }, + { + type: "geojson", + format: "bounding-boxes", + func: SpatialEditor, + }, { type: "array", format: "bounding-box", func: SpatialEditor, }, + { + type: "wkt", + format: "bounding-box", + func: SpatialEditor, + }, + { + type: "geojson", + format: "bounding-box", + func: SpatialEditor, + }, { type: "array", format: "polygons", func: SpatialEditor, }, + { + type: "wkt", + format: "polygons", + func: SpatialEditor, + }, + { + type: "geojson", + format: "polygons", + func: SpatialEditor, + }, { type: "object", format: "polygon", func: SpatialEditor, }, + { + type: "wkt", + format: "polygon", + func: SpatialEditor, + }, + { + type: "geojson", + format: "polygon", + func: SpatialEditor, + }, { type: "array", format: "points", func: SpatialEditor, }, + { + type: "wkt", + format: "points", + func: SpatialEditor, + }, + { + type: "geojson", + format: "points", + func: SpatialEditor, + }, { type: "array", format: "point", func: SpatialEditor, }, + { + type: "wkt", + format: "point", + func: SpatialEditor, + }, + { + type: "geojson", + format: "point", + func: SpatialEditor, + }, { format: "feature", func: SpatialEditor, @@ -48,6 +108,16 @@ const inputs = [ format: "features", func: SpatialEditor, }, + { + type: "wkt", + format: "features", + func: SpatialEditor, + }, + { + type: "geojson", + format: "features", + func: SpatialEditor, + }, ]; /** diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index 1fe271c84..bcb8f735a 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -5,6 +5,8 @@ import { isPoint, isPolygon, isSelection, + isWKT, + isGeoJSON, setAttributes, } from "./utils"; // import "@eox/drawtools"; @@ -60,8 +62,22 @@ export class SpatialEditor extends AbstractEditor { break; } + let format; + switch (true) { + case isWKT(this.schema): + format = "wkt"; + break; + case isGeoJSON(this.schema): + format = "geojson"; + break; + default: + format = "feature"; + break; + } + const attributes = { type: drawType, + format, }; if (this.schema?.options?.projection) { @@ -90,7 +106,7 @@ export class SpatialEditor extends AbstractEditor { style: "width: 100%; height: 300px;", }); this.container.appendChild(eoxmapEl); - attributes.for = "eox-map#" + mapId; + drawtoolsEl.for = eoxmapEl; } setAttributes(drawtoolsEl, attributes); const autoDraw = !(options.autoStartSelection === false); @@ -143,13 +159,26 @@ export class SpatialEditor extends AbstractEditor { (e) => { e.preventDefault(); e.stopPropagation(); - switch (true) { - case !e.detail || !e.detail?.length: { + case !e.detail: { this.value = null; break; } + case isWKT(this.schema): { + // returns the wkt string + this.value = e.detail; + break; + } + case isGeoJSON(this.schema): { + // returns the geojson object + this.value = e.detail; + break; + } case isSelection(this.schema): { + if (!e.detail.length) { + this.value = null; + break; + } /** @param {import("ol/Feature").default} feature */ const getProperty = (feature) => featureProperty @@ -160,20 +189,34 @@ export class SpatialEditor extends AbstractEditor { break; } case isBox(this.schema): { + if (!e.detail.length) { + this.value = null; + break; + } /** @param {import("ol/Feature").default} feature */ const getExtent = (feature) => feature.getGeometry().getExtent(); this.value = spreadFeatures(e.detail, getExtent); break; } - case isPolygon(this.schema): + case isPolygon(this.schema): { + if (!e.detail.length) { + this.value = null; + break; + } this.value = spreadFeatures(e.detail, (feature) => feature); break; - case isPoint(this.schema): + } + case isPoint(this.schema): { + if (!e.detail.length) { + this.value = null; + break; + } this.value = spreadFeatures(e.detail, (feature) => //@ts-expect-error getCoordinates does not exist on Geometry feature.getGeometry()?.getCoordinates(), ); break; + } default: break; } diff --git a/elements/jsonform/src/custom-inputs/spatial/utils.js b/elements/jsonform/src/custom-inputs/spatial/utils.js index 887b96090..674a9ce29 100644 --- a/elements/jsonform/src/custom-inputs/spatial/utils.js +++ b/elements/jsonform/src/custom-inputs/spatial/utils.js @@ -22,6 +22,16 @@ export const isPoint = (schema) => export const isBox = (schema) => ["bounding-boxes", "bounding-box"].some((p) => schema?.format === p); +/** + * Whether a schema has wkt type or not + */ +export const isWKT = (schema) => schema?.type === "wkt"; + +/** + * Whether a schema has geojson type or not + */ +export const isGeoJSON = (schema) => schema?.type === "geojson"; + /** * Whether a schema expects multiple values not */ diff --git a/elements/jsonform/src/custom-inputs/spatial/validator.js b/elements/jsonform/src/custom-inputs/spatial/validator.js index 0c328752f..ae75e59ad 100644 --- a/elements/jsonform/src/custom-inputs/spatial/validator.js +++ b/elements/jsonform/src/custom-inputs/spatial/validator.js @@ -1,10 +1,12 @@ import { isBox, + isGeoJSON, isMulti, isPoint, isPolygon, isSelection, isSupported, + isWKT, satisfiesType, } from "./utils"; @@ -22,7 +24,6 @@ function spatialValidatorCreator(inputs) { * @param {*} schema * @param {*} value * @param {*} path - * @returns {{}} */ return function (schema, value, path) { let errors = []; @@ -51,6 +52,14 @@ function spatialValidatorCreator(inputs) { } switch (true) { + case isWKT(subSchema): { + errors.push(...handleWKT(key, value[key], path)); + break; + } + case isGeoJSON(subSchema): { + errors.push(...handleGeoJson(key, value[key], path)); + break; + } case isSelection(subSchema): { errors.push( ...handleMultiValidation({ @@ -248,3 +257,49 @@ function undefinedValidator(key, val, path) { } return []; } + +function handleWKT(key, val, path) { + // cant be empty geometry + if (typeof val !== "string") { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to be a valid wkt string`, + property: "type", + }, + ]; + } + if (val === "GEOMETRYCOLLECTION EMPTY") { + return [ + { + path: `${path}.${key}`, + message: `Should have at least 1 Geometry`, + property: "type", + }, + ]; + } + return []; +} + +function handleGeoJson(key, val, path) { + // cant be empty geometry + if (typeof val !== "object" || !Object.keys(val).length) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to be a valid geojson object`, + property: "type", + }, + ]; + } + if (!val?.features?.length) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have at least one feature`, + property: "type", + }, + ]; + } + return []; +} diff --git a/elements/jsonform/stories/geojson.js b/elements/jsonform/stories/geojson.js new file mode 100644 index 000000000..a533fc484 --- /dev/null +++ b/elements/jsonform/stories/geojson.js @@ -0,0 +1,27 @@ +/** + * Drawtools component demonstrating the configuration options for eox-jsonform + * Returns drawn features as GeoJSON + */ +import { html } from "lit"; +import geojsonSchema from "./public/geojsonSchema.json"; + +const geoJson = { + args: { + schema: geojsonSchema, + onChange: (e) => { + console.log("value:", e.detail); + }, + }, + render: (args) => html` +

Refer to the console for the returned values

+ + `, +}; + +export default geoJson; diff --git a/elements/jsonform/stories/index.js b/elements/jsonform/stories/index.js index 9efa7049d..e76e1f062 100644 --- a/elements/jsonform/stories/index.js +++ b/elements/jsonform/stories/index.js @@ -8,3 +8,5 @@ export { default as BoundingBoxStory } from "./bounding-box"; // Input form base export { default as PolygonStory } from "./polygons"; // Input form based on drawtools - Polygon export { default as PointStory } from "./points"; // Input form based on drawtools - Point export { default as FeatureSelectionStory } from "./feature-selection"; // Input form based on drawtools - Feature Selection +export { default as WKTStory } from "./wkt"; // Input form based on Drawtools that returns WKT string +export { default as GeoJSONStory } from "./geojson"; // Input form based on Drawtools that returns GeoJSON diff --git a/elements/jsonform/stories/jsonform.stories.js b/elements/jsonform/stories/jsonform.stories.js index 16fd5a069..0182932fb 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -11,6 +11,8 @@ import { FeatureSelectionStory, PointStory, UnStyledStory, + WKTStory, + GeoJSONStory, } from "./index.js"; export default { @@ -71,6 +73,15 @@ export const Points = PointStory; * JSON Form based on drawtools - Feature Selection */ export const FeatureSelection = FeatureSelectionStory; + +/** + * JSON Form based on drawtools - Returns the value as WKT + */ +export const WKT = WKTStory; +/** + * JSON Form based on drawtools - Returns the value as GeoJSON + */ +export const Geojson = GeoJSONStory; /** * Unstyled JSON Form */ diff --git a/elements/jsonform/stories/points.js b/elements/jsonform/stories/points.js index cd818e55f..95145210b 100644 --- a/elements/jsonform/stories/points.js +++ b/elements/jsonform/stories/points.js @@ -1,6 +1,6 @@ /** * Drawtools component demonstrating the configuration options for eox-jsonform - * Allows users to select a bounding box on a map as a form input + * Allows users to select point/points on a map as a form input */ import pointSchema from "./public/pointSchema.json"; diff --git a/elements/jsonform/stories/public/geojsonSchema.json b/elements/jsonform/stories/public/geojsonSchema.json new file mode 100644 index 000000000..9ee4db0ae --- /dev/null +++ b/elements/jsonform/stories/public/geojsonSchema.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "properties": { + "point": { + "title": "Point as Geojson", + "type": "geojson", + "format": "point" + }, + "bounding-boxes": { + "title": "Multiple bounding-boxes as Geojson", + "type": "geojson", + "options": { + "projection": "EPSG:3857" + }, + "format": "bounding-boxes" + }, + "polygon": { + "title": "Polygon as Geojson", + "type": "geojson", + "options": { + "projection": "EPSG:3857" + }, + "format": "polygon" + } + } +} diff --git a/elements/jsonform/stories/public/wktSchema.json b/elements/jsonform/stories/public/wktSchema.json new file mode 100644 index 000000000..435d560d7 --- /dev/null +++ b/elements/jsonform/stories/public/wktSchema.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "properties": { + "point": { + "title": "point as WKT", + "type": "wkt", + "format": "point" + }, + "bounding-boxes": { + "title": "Multiple bounding boxes as WKT", + "type": "wkt", + "options": { + "projection": "EPSG:3857" + }, + "format": "bounding-boxes" + }, + "polygon": { + "title": "Polygon as WKT", + "type": "wkt", + "options": { + "projection": "EPSG:3857" + }, + "format": "polygon" + } + } +} diff --git a/elements/jsonform/stories/wkt.js b/elements/jsonform/stories/wkt.js new file mode 100644 index 000000000..dea7f7f30 --- /dev/null +++ b/elements/jsonform/stories/wkt.js @@ -0,0 +1,27 @@ +/** + * Drawtools component demonstrating the configuration options for eox-jsonform + * Returns drawn features as WKT + */ +import { html } from "lit"; +import wktSchema from "./public/wktSchema.json"; + +const wkt = { + args: { + schema: wktSchema, + onChange: (e) => { + console.log("value:", e.detail); + }, + }, + render: (args) => html` +

Refer to the console for the returned values

+ + `, +}; + +export default wkt; From 33e2c36dda4fd02c9cab71476a99502401f274e3 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Wed, 27 Nov 2024 14:21:12 +0100 Subject: [PATCH 19/24] chore: update stories and clean up --- .../jsonform/src/custom-inputs/spatial/validator.js | 2 -- elements/jsonform/stories/feature-selection.js | 2 +- elements/jsonform/stories/public/featureSchema.json | 11 +++++------ elements/jsonform/stories/public/geojsonSchema.json | 4 ++-- elements/jsonform/stories/public/wktSchema.json | 4 ++-- package-lock.json | 2 +- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial/validator.js b/elements/jsonform/src/custom-inputs/spatial/validator.js index ae75e59ad..d37193d04 100644 --- a/elements/jsonform/src/custom-inputs/spatial/validator.js +++ b/elements/jsonform/src/custom-inputs/spatial/validator.js @@ -259,7 +259,6 @@ function undefinedValidator(key, val, path) { } function handleWKT(key, val, path) { - // cant be empty geometry if (typeof val !== "string") { return [ { @@ -282,7 +281,6 @@ function handleWKT(key, val, path) { } function handleGeoJson(key, val, path) { - // cant be empty geometry if (typeof val !== "object" || !Object.keys(val).length) { return [ { diff --git a/elements/jsonform/stories/feature-selection.js b/elements/jsonform/stories/feature-selection.js index 5b3aafd14..b0458ad2c 100644 --- a/elements/jsonform/stories/feature-selection.js +++ b/elements/jsonform/stories/feature-selection.js @@ -13,7 +13,7 @@ import featureSchema from "./public/featureSchema.json"; const FeatureSelection = { args: { schema: featureSchema, - onChange: (e) => console.log("change event", e.detail), + onChange: (e) => console.log("value:", e.detail), }, render: (args) => html` Date: Wed, 27 Nov 2024 14:45:07 +0100 Subject: [PATCH 20/24] test: adjust tests --- elements/jsonform/test/_mockedDrawtools.js | 3 +++ elements/jsonform/test/cases/render-drawtools.js | 4 ++-- elements/jsonform/test/fixtures/spatialSchema.json | 4 ++-- elements/jsonform/test/general.cy.js | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/elements/jsonform/test/_mockedDrawtools.js b/elements/jsonform/test/_mockedDrawtools.js index 0e54a0c75..d17b42d59 100644 --- a/elements/jsonform/test/_mockedDrawtools.js +++ b/elements/jsonform/test/_mockedDrawtools.js @@ -17,6 +17,9 @@ class MockedDrawTools extends HTMLElement { this["show-list"] = false; this.projection = "EPSG:4326"; this.type = ""; + this.format = "feature"; + this.updateComplete = new Promise((resolve) => resolve(true)); + this.startDrawing = () => {}; } } diff --git a/elements/jsonform/test/cases/render-drawtools.js b/elements/jsonform/test/cases/render-drawtools.js index f1ed81035..5822ea017 100644 --- a/elements/jsonform/test/cases/render-drawtools.js +++ b/elements/jsonform/test/cases/render-drawtools.js @@ -4,7 +4,7 @@ import schemaFixture from "../fixtures/spatialSchema.json"; // Destructure TEST_SELECTORS object const { jsonForm } = TEST_SELECTORS; -const checkDrawtoolsForTypeSpatial = () => { +const checkDrawtoolsForSpatialEditor = () => { cy.intercept("**/spatialSchema.json", (req) => { req.reply(schemaFixture); }); @@ -47,4 +47,4 @@ const checkDrawtoolsForTypeSpatial = () => { }); }; -export default checkDrawtoolsForTypeSpatial; +export default checkDrawtoolsForSpatialEditor; diff --git a/elements/jsonform/test/fixtures/spatialSchema.json b/elements/jsonform/test/fixtures/spatialSchema.json index 0324189fe..851da4b1c 100644 --- a/elements/jsonform/test/fixtures/spatialSchema.json +++ b/elements/jsonform/test/fixtures/spatialSchema.json @@ -2,7 +2,7 @@ "type": "object", "properties": { "bbox": { - "type": "spatial", + "type": "array", "options": { "for": "mocked-map#bbox", "projection": "EPSG:3857" @@ -10,7 +10,7 @@ "format": "bounding-box" }, "polygons": { - "type": "spatial", + "type": "array", "options": { "for": "mocked-map#polygons" }, diff --git a/elements/jsonform/test/general.cy.js b/elements/jsonform/test/general.cy.js index 4b986e0d2..284cef06a 100644 --- a/elements/jsonform/test/general.cy.js +++ b/elements/jsonform/test/general.cy.js @@ -27,5 +27,5 @@ describe("Jsonform", () => { it("triggers a change event when typing", () => triggerChangeEventTest()); it("loads values", () => loadValuesTest()); it("loads mismatching values", () => loadMisMatchingValuesTest()); - it("renders drawtools on type spatial", () => renderDrawtools()); + it.only("renders drawtools as a custom input", () => renderDrawtools()); }); From 09e5f5ee262dee3cdada2ff586501ac64f41370c Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Fri, 29 Nov 2024 13:58:16 +0100 Subject: [PATCH 21/24] fix: return a feature for single formats and feature collection for plural --- .../src/custom-inputs/spatial/editor.js | 8 ++- .../src/custom-inputs/spatial/validator.js | 55 +++++++++++++++---- .../stories/public/featureSchema.json | 2 +- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index bcb8f735a..ba28a122c 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -170,8 +170,12 @@ export class SpatialEditor extends AbstractEditor { break; } case isGeoJSON(this.schema): { - // returns the geojson object - this.value = e.detail; + const featureCollection = e.detail; + if (isMulti(this.schema)) { + this.value = featureCollection; + break; + } + this.value = featureCollection.features?.[0] ?? null; break; } case isSelection(this.schema): { diff --git a/elements/jsonform/src/custom-inputs/spatial/validator.js b/elements/jsonform/src/custom-inputs/spatial/validator.js index d37193d04..ae541ab5a 100644 --- a/elements/jsonform/src/custom-inputs/spatial/validator.js +++ b/elements/jsonform/src/custom-inputs/spatial/validator.js @@ -53,11 +53,11 @@ function spatialValidatorCreator(inputs) { switch (true) { case isWKT(subSchema): { - errors.push(...handleWKT(key, value[key], path)); + errors.push(...wktValidator(key, value[key], path)); break; } case isGeoJSON(subSchema): { - errors.push(...handleGeoJson(key, value[key], path)); + errors.push(...geoJsonValidator(key, value[key], path, subSchema)); break; } case isSelection(subSchema): { @@ -258,7 +258,7 @@ function undefinedValidator(key, val, path) { return []; } -function handleWKT(key, val, path) { +function wktValidator(key, val, path) { if (typeof val !== "string") { return [ { @@ -280,7 +280,7 @@ function handleWKT(key, val, path) { return []; } -function handleGeoJson(key, val, path) { +function geoJsonValidator(key, val, path, subSchema) { if (typeof val !== "object" || !Object.keys(val).length) { return [ { @@ -290,14 +290,45 @@ function handleGeoJson(key, val, path) { }, ]; } - if (!val?.features?.length) { - return [ - { - path: `${path}.${key}`, - message: `Value is expected to have at least one feature`, - property: "type", - }, - ]; + + if (isMulti(subSchema)) { + if (val.type !== "FeatureCollection") { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to be a valid FeaturesCollection geojson`, + property: "type", + }, + ]; + } + if (!val?.features?.length) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have at least one feature`, + property: "type", + }, + ]; + } + } else { + if (val.type !== "Feature") { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to be a Feature geojson`, + property: "type", + }, + ]; + } + if (!val?.geometry.type) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have a valid geometry`, + property: "type", + }, + ]; + } } return []; } diff --git a/elements/jsonform/stories/public/featureSchema.json b/elements/jsonform/stories/public/featureSchema.json index febb249b1..1e9d3f0fa 100644 --- a/elements/jsonform/stories/public/featureSchema.json +++ b/elements/jsonform/stories/public/featureSchema.json @@ -2,7 +2,7 @@ "type": "object", "properties": { "features": { - "title": "Select multipule features from the map, and refer to the console for the returned values", + "title": "Select multiple features from the map, and refer to the console for the returned values", "type": "array", "options": { "featureProperty": "BIOME_NAME", From 76f93d55a33e76f4e666191d77d3f8f125a0093d Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Fri, 29 Nov 2024 15:25:57 +0100 Subject: [PATCH 22/24] feat: support line/lines format --- elements/jsonform/src/custom-inputs/index.js | 30 ++++++++++++ .../src/custom-inputs/spatial/editor.js | 11 +++++ .../src/custom-inputs/spatial/utils.js | 22 ++++++--- .../src/custom-inputs/spatial/validator.js | 48 +++++++++++++++---- elements/jsonform/stories/index.js | 1 + elements/jsonform/stories/jsonform.stories.js | 6 +++ elements/jsonform/stories/line.js | 12 +++++ .../stories/public/featureSchema.json | 2 +- .../stories/public/geojsonSchema.json | 5 ++ .../jsonform/stories/public/lineSchema.json | 18 +++++++ 10 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 elements/jsonform/stories/line.js create mode 100644 elements/jsonform/stories/public/lineSchema.json diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index 5ce98f78d..5c7f06f0b 100644 --- a/elements/jsonform/src/custom-inputs/index.js +++ b/elements/jsonform/src/custom-inputs/index.js @@ -118,6 +118,36 @@ const inputs = [ format: "features", func: SpatialEditor, }, + { + type: "array", + format: "line", + func: SpatialEditor, + }, + { + type: "wkt", + format: "line", + func: SpatialEditor, + }, + { + type: "geojson", + format: "line", + func: SpatialEditor, + }, + { + type: "array", + format: "lines", + func: SpatialEditor, + }, + { + type: "wkt", + format: "lines", + func: SpatialEditor, + }, + { + type: "geojson", + format: "lines", + func: SpatialEditor, + }, ]; /** diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index ba28a122c..f028e2dc2 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -8,6 +8,7 @@ import { isWKT, isGeoJSON, setAttributes, + isLine, } from "./utils"; // import "@eox/drawtools"; @@ -57,6 +58,9 @@ export class SpatialEditor extends AbstractEditor { case isPoint(this.schema): drawType = "Point"; break; + case isLine(this.schema): + drawType = "LineString"; + break; default: drawType = "Box"; break; @@ -178,6 +182,13 @@ export class SpatialEditor extends AbstractEditor { this.value = featureCollection.features?.[0] ?? null; break; } + case isLine(this.schema): { + this.value = spreadFeatures(e.detail, (feature) => + //@ts-expect-error getCoordinates does not exist on Geometry + feature.getGeometry().getCoordinates(), + ); + break; + } case isSelection(this.schema): { if (!e.detail.length) { this.value = null; diff --git a/elements/jsonform/src/custom-inputs/spatial/utils.js b/elements/jsonform/src/custom-inputs/spatial/utils.js index 674a9ce29..6cee1c201 100644 --- a/elements/jsonform/src/custom-inputs/spatial/utils.js +++ b/elements/jsonform/src/custom-inputs/spatial/utils.js @@ -8,19 +8,23 @@ export const isSelection = (schema) => * Whether a schema has ploygon/polygons format or not */ export const isPolygon = (schema) => - ["polygon", "polygons"].some((p) => schema?.format === p); + ["polygon", "polygons"].includes(schema?.format); /** * Whether a schema has point/points format or not */ -export const isPoint = (schema) => - ["point", "points"].some((p) => schema?.format === p); +export const isPoint = (schema) => ["point", "points"].includes(schema?.format); /** * Whether a schema has bbox/bboxes format or not */ export const isBox = (schema) => - ["bounding-boxes", "bounding-box"].some((p) => schema?.format === p); + ["bounding-boxes", "bounding-box"].includes(schema?.format); + +/** + * Whether a schema has line/lines format or not + */ +export const isLine = (schema) => ["lines", "line"].includes(schema?.format); /** * Whether a schema has wkt type or not @@ -36,15 +40,19 @@ export const isGeoJSON = (schema) => schema?.type === "geojson"; * Whether a schema expects multiple values not */ export const isMulti = (schema) => - ["bounding-boxes", "polygons", "features", "points"].some( - (m) => schema?.format === m, + ["bounding-boxes", "polygons", "features", "points", "lines"].includes( + schema?.format, ); /** * Whether a schema is supported by the spatial editor **/ export const isSupported = (schema) => - isSelection(schema) || isPolygon(schema) || isBox(schema) || isPoint(schema); + isSelection(schema) || + isPolygon(schema) || + isBox(schema) || + isPoint(schema) || + isLine(schema); /** * Set multiple attributes to an element diff --git a/elements/jsonform/src/custom-inputs/spatial/validator.js b/elements/jsonform/src/custom-inputs/spatial/validator.js index ae541ab5a..d96a40d02 100644 --- a/elements/jsonform/src/custom-inputs/spatial/validator.js +++ b/elements/jsonform/src/custom-inputs/spatial/validator.js @@ -1,6 +1,7 @@ import { isBox, isGeoJSON, + isLine, isMulti, isPoint, isPolygon, @@ -108,6 +109,18 @@ function spatialValidatorCreator(inputs) { ); break; } + case isLine(subSchema): { + errors.push( + ...handleMultiValidation({ + key, + subValue: value[key], + subSchema, + path, + validationFn: lineValidator, + }), + ); + break; + } default: break; } @@ -245,6 +258,32 @@ function pointValidator(key, val, path) { return errors; } +function lineValidator(key, val, path) { + // expect to return line coordinates + const errors = []; + if (val.length < 2) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have at least 2 points but got ${val.length}`, + property: "format", + }, + ]; + } + val.forEach((points, i) => { + points.forEach((point, j) => { + if (typeof point !== "number") { + errors.push({ + path: `${path}.${key}.${i}.${j}`, + message: `coordinates is expected to be of type number but got ${point}`, + property: "format", + }); + } + }); + }); + return errors; +} + function undefinedValidator(key, val, path) { if (!val) { return [ @@ -311,15 +350,6 @@ function geoJsonValidator(key, val, path, subSchema) { ]; } } else { - if (val.type !== "Feature") { - return [ - { - path: `${path}.${key}`, - message: `Value is expected to be a Feature geojson`, - property: "type", - }, - ]; - } if (!val?.geometry.type) { return [ { diff --git a/elements/jsonform/stories/index.js b/elements/jsonform/stories/index.js index e76e1f062..c4e92bdfe 100644 --- a/elements/jsonform/stories/index.js +++ b/elements/jsonform/stories/index.js @@ -8,5 +8,6 @@ export { default as BoundingBoxStory } from "./bounding-box"; // Input form base export { default as PolygonStory } from "./polygons"; // Input form based on drawtools - Polygon export { default as PointStory } from "./points"; // Input form based on drawtools - Point export { default as FeatureSelectionStory } from "./feature-selection"; // Input form based on drawtools - Feature Selection +export { default as LineStory } from "./line"; // Input form based on Drawtools - LineString export { default as WKTStory } from "./wkt"; // Input form based on Drawtools that returns WKT string export { default as GeoJSONStory } from "./geojson"; // Input form based on Drawtools that returns GeoJSON diff --git a/elements/jsonform/stories/jsonform.stories.js b/elements/jsonform/stories/jsonform.stories.js index 0182932fb..9fe87ae8d 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -13,6 +13,7 @@ import { UnStyledStory, WKTStory, GeoJSONStory, + LineStory, } from "./index.js"; export default { @@ -69,6 +70,11 @@ export const Polygons = PolygonStory; */ export const Points = PointStory; +/** + * JSON Form based on drawtools - LineString + * + */ +export const Line = LineStory; /** * JSON Form based on drawtools - Feature Selection */ diff --git a/elements/jsonform/stories/line.js b/elements/jsonform/stories/line.js new file mode 100644 index 000000000..f4b138935 --- /dev/null +++ b/elements/jsonform/stories/line.js @@ -0,0 +1,12 @@ +/** + * Drawtools component demonstrating the configuration options for eox-jsonform + * Allows users to draw a line/lines on a map as a form input + */ +import lineSchema from "./public/lineSchema.json"; + +const Line = { + args: { + schema: lineSchema, + }, +}; +export default Line; diff --git a/elements/jsonform/stories/public/featureSchema.json b/elements/jsonform/stories/public/featureSchema.json index 1e9d3f0fa..540c30376 100644 --- a/elements/jsonform/stories/public/featureSchema.json +++ b/elements/jsonform/stories/public/featureSchema.json @@ -15,7 +15,7 @@ "format": "features" }, "feature": { - "title": "Select a Feature from the map", + "title": "Select a feature from the map", "type": "object", "options": { "layerId": "regions-blue", diff --git a/elements/jsonform/stories/public/geojsonSchema.json b/elements/jsonform/stories/public/geojsonSchema.json index cb356fca8..8fb38cb14 100644 --- a/elements/jsonform/stories/public/geojsonSchema.json +++ b/elements/jsonform/stories/public/geojsonSchema.json @@ -6,6 +6,11 @@ "type": "geojson", "format": "point" }, + "line": { + "title": "LineString as Geojson", + "type": "geojson", + "format": "line" + }, "bounding-boxes": { "title": "Multiple bounding-boxes as Geojson in EPSG:3857", "type": "geojson", diff --git a/elements/jsonform/stories/public/lineSchema.json b/elements/jsonform/stories/public/lineSchema.json new file mode 100644 index 000000000..2c286d53c --- /dev/null +++ b/elements/jsonform/stories/public/lineSchema.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "properties": { + "line": { + "title": "Single line example", + "type": "array", + "format": "line" + }, + "lines": { + "title": "Multiple lines example", + "type": "array", + "options": { + "projection": "EPSG:3857" + }, + "format": "lines" + } + } +} From c6abbd9e4396b5822fdb467d8bc37016cd59b7c8 Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Mon, 2 Dec 2024 11:08:15 +0100 Subject: [PATCH 23/24] fix: discard drawing on destroy --- elements/jsonform/src/custom-inputs/spatial/editor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index f028e2dc2..6081dc47e 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -251,6 +251,7 @@ export class SpatialEditor extends AbstractEditor { this.description.parentNode.removeChild(this.description); if (this.input && this.input.parentNode) { this.input.parentNode.removeChild(this.input); + this.input.discardDrawing() this.input.remove(); } super.destroy(); From 3af6909e3399cf1e59c33f246f38015b20d8adef Mon Sep 17 00:00:00 2001 From: A-Behairi Date: Wed, 4 Dec 2024 09:41:25 +0100 Subject: [PATCH 24/24] fix: adjust collection schema and format --- elements/jsonform/src/custom-inputs/spatial/editor.js | 2 +- .../jsonform/stories/public/collectionSchema.json | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/elements/jsonform/src/custom-inputs/spatial/editor.js b/elements/jsonform/src/custom-inputs/spatial/editor.js index 6081dc47e..eed32df73 100644 --- a/elements/jsonform/src/custom-inputs/spatial/editor.js +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -251,7 +251,7 @@ export class SpatialEditor extends AbstractEditor { this.description.parentNode.removeChild(this.description); if (this.input && this.input.parentNode) { this.input.parentNode.removeChild(this.input); - this.input.discardDrawing() + this.input.discardDrawing(); this.input.remove(); } super.destroy(); diff --git a/elements/jsonform/stories/public/collectionSchema.json b/elements/jsonform/stories/public/collectionSchema.json index 8d03d6a92..5611c5676 100644 --- a/elements/jsonform/stories/public/collectionSchema.json +++ b/elements/jsonform/stories/public/collectionSchema.json @@ -627,12 +627,19 @@ "spatial": { "title": "Spatial Extents", "type": "object", - "format": "bounding-boxes", "required": ["bbox"], "properties": { "bbox": { "type": "array", - "format": "bounding-boxes" + "format": "bounding-boxes", + "items": { + "type": "array", + "format": "bounding-box", + "items": { "type": "number" }, + "minItems": 4, + "maxItems": 4 + }, + "minItems": 1 } } },