diff --git a/elements/jsonform/src/custom-inputs/index.js b/elements/jsonform/src/custom-inputs/index.js index 2f4b1065f..5c7f06f0b 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 { SpatialEditor, spatialValidatorCreator } from "./spatial"; // Define custom input types const inputs = [ @@ -8,6 +9,145 @@ const inputs = [ format: "minmax", func: MinMaxEditor, }, + { + type: "array", + 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, + }, + { + type: "array", + format: "features", + func: SpatialEditor, + }, + { + type: "wkt", + format: "features", + func: SpatialEditor, + }, + { + type: "geojson", + 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, + }, ]; /** @@ -16,6 +156,11 @@ 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( + spatialValidatorCreator(inputs), + ); + // Iterate over each custom input definition inputs.map(({ type, format, func }) => { JSONEditor.defaults["startVals"] = startVals; @@ -24,6 +169,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 new file mode 100644 index 000000000..eed32df73 --- /dev/null +++ b/elements/jsonform/src/custom-inputs/spatial/editor.js @@ -0,0 +1,259 @@ +import { AbstractEditor } from "@json-editor/json-editor/src/editor.js"; +import { + isBox, + isMulti, + isPoint, + isPolygon, + isSelection, + isWKT, + isGeoJSON, + setAttributes, + isLine, +} from "./utils"; +// import "@eox/drawtools"; + +// Define a custom editor class extending AbstractEditor +export class SpatialEditor extends AbstractEditor { + register() { + super.register(); + } + + unregister() { + super.unregister(); + } + + // Build the editor UI + build() { + const options = this.options; + const description = this.schema.description; + const theme = this.theme; + + // 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 = /** @type {import("@eox/drawtools").EOxDrawTools} */ ( + document.createElement("eox-drawtools") + ); + + 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; + case isLine(this.schema): + drawType = "LineString"; + break; + default: + drawType = "Box"; + 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) { + attributes.projection = this.schema.options.projection; + } + + if (isSelection(this.schema)) { + attributes["layer-id"] = this.schema.options.layerId; + } + if (isMulti(this.schema)) { + attributes["multiple-features"] = true; + attributes["show-list"] = true; + } + + if ("for" in (this.schema.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 = eoxmapEl; + } + setAttributes(drawtoolsEl, attributes); + const autoDraw = !(options.autoStartSelection === false); + if (autoDraw) { + drawtoolsEl.updateComplete.then(() => { + drawtoolsEl.startDrawing(); + }); + } + + 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; + } + + 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 (!isMulti(this.schema) && 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", + /** @type {EventListener} */ ( + /** + * @param {CustomEvent} e + */ + (e) => { + e.preventDefault(); + e.stopPropagation(); + switch (true) { + case !e.detail: { + this.value = null; + break; + } + case isWKT(this.schema): { + // returns the wkt string + this.value = e.detail; + break; + } + case isGeoJSON(this.schema): { + const featureCollection = e.detail; + if (isMulti(this.schema)) { + this.value = featureCollection; + break; + } + 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; + break; + } + /** @param {import("ol/Feature").default} feature */ + const getProperty = (feature) => + featureProperty + ? (feature.get(featureProperty) ?? feature) + : feature; + + this.value = spreadFeatures(e.detail, getProperty); + 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): { + if (!e.detail.length) { + this.value = null; + break; + } + this.value = spreadFeatures(e.detail, (feature) => feature); + break; + } + 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; + } + + 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); + this.input.discardDrawing(); + this.input.remove(); + } + super.destroy(); + } +} 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..5937a58c3 --- /dev/null +++ b/elements/jsonform/src/custom-inputs/spatial/index.js @@ -0,0 +1,2 @@ +export { SpatialEditor } from "./editor"; +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 new file mode 100644 index 000000000..6cee1c201 --- /dev/null +++ b/elements/jsonform/src/custom-inputs/spatial/utils.js @@ -0,0 +1,99 @@ +/** + * Whether a schema has feature/feature format or not + */ +export const isSelection = (schema) => + ["feature", "features"].some((f) => schema?.format === f); + +/** + * Whether a schema has ploygon/polygons format or not + */ +export const isPolygon = (schema) => + ["polygon", "polygons"].includes(schema?.format); + +/** + * Whether a schema has point/points format or not + */ +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"].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 + */ +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 + */ +export const isMulti = (schema) => + ["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) || + isLine(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..d96a40d02 --- /dev/null +++ b/elements/jsonform/src/custom-inputs/spatial/validator.js @@ -0,0 +1,364 @@ +import { + isBox, + isGeoJSON, + isLine, + isMulti, + isPoint, + isPolygon, + isSelection, + isSupported, + isWKT, + satisfiesType, +} from "./utils"; + +/** + * @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 + */ + return function (schema, value, path) { + let errors = []; + if (!schema.properties) { + return errors; + } + + 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, + )); + + if (!toBeValidated) { + // only validate defined types and formats using the spatial editor + return; + } + + const undefinedError = undefinedValidator(key, value[key], path); + if (undefinedError.length) { + errors.push(...undefinedError); + return; + } + + switch (true) { + case isWKT(subSchema): { + errors.push(...wktValidator(key, value[key], path)); + break; + } + case isGeoJSON(subSchema): { + errors.push(...geoJsonValidator(key, value[key], path, subSchema)); + break; + } + 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; + } + case isPoint(subSchema): { + errors.push( + ...handleMultiValidation({ + key, + subValue: value[key], + subSchema, + path, + validationFn: pointValidator, + }), + ); + break; + } + case isLine(subSchema): { + errors.push( + ...handleMultiValidation({ + key, + subValue: value[key], + subSchema, + path, + validationFn: lineValidator, + }), + ); + break; + } + default: + break; + } + }); + return errors; + }; +} +export default spatialValidatorCreator; +/** + * 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 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); + } +} + +/** + * Bounding box validator + */ +function bBoxValidator(key, val, path) { + // expect to return the spatial extent + const errors = []; + if (val.length !== 4) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have 4 items 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) { + 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 a valid ${expected}`, + 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 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 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 [ + { + path: `${path}.${key}`, + message: `invalid value ${JSON.stringify(val)}`, + property: "type", + }, + ]; + } + return []; +} + +function wktValidator(key, val, path) { + 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 geoJsonValidator(key, val, path, subSchema) { + 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 (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?.geometry.type) { + return [ + { + path: `${path}.${key}`, + message: `Value is expected to have a valid geometry`, + property: "type", + }, + ]; + } + } + return []; +} diff --git a/elements/jsonform/src/enums/index.js b/elements/jsonform/src/enums/index.js index 526db404c..dc6fe499d 100644 --- a/elements/jsonform/src/enums/index.js +++ b/elements/jsonform/src/enums/index.js @@ -1 +1,6 @@ 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..c3a665741 --- /dev/null +++ b/elements/jsonform/src/enums/stories.js @@ -0,0 +1,43 @@ +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", + }, + }, +]; 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/feature-selection.js b/elements/jsonform/stories/feature-selection.js new file mode 100644 index 000000000..b0458ad2c --- /dev/null +++ b/elements/jsonform/stories/feature-selection.js @@ -0,0 +1,40 @@ +import { html } from "lit"; +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 + */ +import featureSchema from "./public/featureSchema.json"; + +const FeatureSelection = { + args: { + schema: featureSchema, + onChange: (e) => console.log("value:", e.detail), + }, + render: (args) => html` + + + + + + `, +}; +export default FeatureSelection; 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 9561faef6..c4e92bdfe 100644 --- a/elements/jsonform/stories/index.js +++ b/elements/jsonform/stories/index.js @@ -4,3 +4,10 @@ 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 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 +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 63755ea5f..9fe87ae8d 100644 --- a/elements/jsonform/stories/jsonform.stories.js +++ b/elements/jsonform/stories/jsonform.stories.js @@ -6,7 +6,14 @@ import { ExternalStory, MarkdownStory, PrimaryStory, + BoundingBoxStory, + PolygonStory, + FeatureSelectionStory, + PointStory, UnStyledStory, + WKTStory, + GeoJSONStory, + LineStory, } from "./index.js"; export default { @@ -48,6 +55,39 @@ export const External = ExternalStory; */ export const Markdown = MarkdownStory; +/** + * JSON Form based on drawtools - Box + */ +export const BoundigBox = BoundingBoxStory; + +/** + * JSON Form based on drawtools - Polygon + */ +export const Polygons = PolygonStory; + +/** + * JSON Form based on drawtools - Point + */ +export const Points = PointStory; + +/** + * JSON Form based on drawtools - LineString + * + */ +export const Line = LineStory; +/** + * 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/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/points.js b/elements/jsonform/stories/points.js new file mode 100644 index 000000000..95145210b --- /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 point/points 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/polygons.js b/elements/jsonform/stories/polygons.js new file mode 100644 index 000000000..41e61f067 --- /dev/null +++ b/elements/jsonform/stories/polygons.js @@ -0,0 +1,12 @@ +/** + * 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"; + +const Polygons = { + args: { + schema: polygonsScheme, + }, +}; +export default Polygons; diff --git a/elements/jsonform/stories/public/boundingBoxSchema.json b/elements/jsonform/stories/public/boundingBoxSchema.json new file mode 100644 index 000000000..5c29baba4 --- /dev/null +++ b/elements/jsonform/stories/public/boundingBoxSchema.json @@ -0,0 +1,25 @@ +{ + "type": "object", + "properties": { + "bboxes": { + "title": "Multi bbox example", + "type": "array", + "properties": {}, + "format": "bounding-boxes" + }, + "bbox": { + "title": "Single bbox example", + "type": "array", + "properties": {}, + "format": "bounding-box" + }, + "bboxes2": { + "title": "Multi bbox example without auto start drawing", + "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 287c83498..5611c5676 100644 --- a/elements/jsonform/stories/public/collectionSchema.json +++ b/elements/jsonform/stories/public/collectionSchema.json @@ -627,11 +627,11 @@ "spatial": { "title": "Spatial Extents", "type": "object", - "format": "bounding-boxes", "required": ["bbox"], "properties": { "bbox": { "type": "array", + "format": "bounding-boxes", "items": { "type": "array", "format": "bounding-box", diff --git a/elements/jsonform/stories/public/featureSchema.json b/elements/jsonform/stories/public/featureSchema.json new file mode 100644 index 000000000..540c30376 --- /dev/null +++ b/elements/jsonform/stories/public/featureSchema.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "properties": { + "features": { + "title": "Select multiple features from the map, and refer to the console for the returned values", + "type": "array", + "options": { + "featureProperty": "BIOME_NAME", + "layerId": "regions-grey", + "for": "eox-map#first" + }, + "items": { + "type": "string" + }, + "format": "features" + }, + "feature": { + "title": "Select a feature from the map", + "type": "object", + "options": { + "layerId": "regions-blue", + "for": "eox-map#second" + }, + "format": "feature" + } + } +} diff --git a/elements/jsonform/stories/public/geojsonSchema.json b/elements/jsonform/stories/public/geojsonSchema.json new file mode 100644 index 000000000..8fb38cb14 --- /dev/null +++ b/elements/jsonform/stories/public/geojsonSchema.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "properties": { + "point": { + "title": "Point as Geojson", + "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", + "options": { + "projection": "EPSG:3857" + }, + "format": "bounding-boxes" + }, + "polygon": { + "title": "Polygon as Geojson in EPSG:3857", + "type": "geojson", + "options": { + "projection": "EPSG:3857" + }, + "format": "polygon" + } + } +} 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" + } + } +} 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" + } + } +} diff --git a/elements/jsonform/stories/public/polygonSchema.json b/elements/jsonform/stories/public/polygonSchema.json new file mode 100644 index 000000000..6a93460b9 --- /dev/null +++ b/elements/jsonform/stories/public/polygonSchema.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "polygons": { + "title": "Multi polygon", + "type": "array", + "properties": {}, + "format": "polygons" + }, + "polygon": { + "title": "Single polygon", + "type": "object", + "properties": {}, + "format": "polygon" + } + } +} diff --git a/elements/jsonform/stories/public/wktSchema.json b/elements/jsonform/stories/public/wktSchema.json new file mode 100644 index 000000000..07ba8ffd1 --- /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 in EPSG:3857", + "type": "wkt", + "options": { + "projection": "EPSG:3857" + }, + "format": "bounding-boxes" + }, + "polygon": { + "title": "Polygon as WKT in EPSG:3857", + "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; diff --git a/elements/jsonform/test/_mockedDrawtools.js b/elements/jsonform/test/_mockedDrawtools.js new file mode 100644 index 000000000..d17b42d59 --- /dev/null +++ b/elements/jsonform/test/_mockedDrawtools.js @@ -0,0 +1,26 @@ +class MockedDrawTools extends HTMLElement { + static observedAttributes = [ + "for", + "layer-id", + "multiple-features", + "show-editor", + "show-list", + "type", + "projection", + ]; + constructor() { + super(); + this.for = ""; + this["layer-id"] = ""; + this["multiple-features"] = false; + this["show-editor"] = false; + this["show-list"] = false; + this.projection = "EPSG:4326"; + this.type = ""; + this.format = "feature"; + this.updateComplete = new Promise((resolve) => resolve(true)); + this.startDrawing = () => {}; + } +} + +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..5822ea017 --- /dev/null +++ b/elements/jsonform/test/cases/render-drawtools.js @@ -0,0 +1,50 @@ +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 checkDrawtoolsForSpatialEditor = () => { + 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"); + // 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) => { + // 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 checkDrawtoolsForSpatialEditor; 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 diff --git a/elements/jsonform/test/fixtures/spatialSchema.json b/elements/jsonform/test/fixtures/spatialSchema.json new file mode 100644 index 000000000..851da4b1c --- /dev/null +++ b/elements/jsonform/test/fixtures/spatialSchema.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "properties": { + "bbox": { + "type": "array", + "options": { + "for": "mocked-map#bbox", + "projection": "EPSG:3857" + }, + "format": "bounding-box" + }, + "polygons": { + "type": "array", + "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..284cef06a 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.only("renders drawtools as a custom input", () => renderDrawtools()); }); diff --git a/package-lock.json b/package-lock.json index d72837b05..ea14c54be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ }, "elements/drawtools": { "name": "@eox/drawtools", - "version": "0.12.0", + "version": "0.13.0", "dependencies": { "@eox/elements-utils": "^0.1.1", "lit": "^3.2.0",