Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added initial bounding box format handler for jsonform #1276

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
54 changes: 54 additions & 0 deletions elements/jsonform/src/custom-inputs/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { JSONEditor } from "@json-editor/json-editor/src/core.js";
import { MinMaxEditor } from "./minmax";
import { SpatialEditor, spatialValidator } from "./spatial";

// Define custom input types
const inputs = [
Expand All @@ -8,6 +9,56 @@ const inputs = [
format: "minmax",
func: MinMaxEditor,
},
{
type: "spatial",
format: "bounding-boxes",
func: SpatialEditor,
},
{
type: "spatial",
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",
func: SpatialEditor,
},
{
type: "spatial",
format: "polygon",
func: SpatialEditor,
},
{
type: "spatial",
format: "polygons-editor",
func: SpatialEditor,
},
{
type: "spatial",
format: "polygon-editor",
func: SpatialEditor,
},
{
type: "spatial",
format: "feature",
func: SpatialEditor,
},
{
type: "spatial",
format: "features",
func: SpatialEditor,
},
];

/**
Expand All @@ -16,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;
Expand Down
172 changes: 172 additions & 0 deletions elements/jsonform/src/custom-inputs/spatial/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { AbstractEditor } from "@json-editor/json-editor/src/editor.js";
import { isBox, isMulti, isPolygon, isSelection, setAttributes } 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 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 enableEditor = this.schema.format.includes("editor");

const drawType = isPolygon(this.schema) ? "Polygon" : "Box";
const attributes = {
type: drawType,
};
if (isSelection(this.schema)) {
attributes["layer-id"] = this.schema.options.layerId;
}
if (isMulti(this.schema)) {
attributes["multiple-features"] = true;
}

if (enableEditor) {
attributes["import-features"] = true;
attributes["show-editor"] = true;
}

if (isMulti(this.schema)) {
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);
attributes.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;
}

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<import("ol/Feature").default|import("ol/Feature").default[]>} e
*/
(e) => {
e.preventDefault();
e.stopPropagation();

switch (true) {
case !e.detail || e.detail?.length === 0: {
this.value = null;
break;
}
case isSelection(this.schema): {
/** @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): {
/** @param {import("ol/Feature").default} feature */
const getExtent = (feature) => feature.getGeometry().getExtent();
this.value = spreadFeatures(e.detail, getExtent);
break;
}
case isPolygon(this.schema):
this.value = spreadFeatures(e.detail, (feature) => feature);
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.remove();
}
super.destroy();
}
}
2 changes: 2 additions & 0 deletions elements/jsonform/src/custom-inputs/spatial/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SpatialEditor } from "./editor";
export { default as spatialValidator } from "./validator";
75 changes: 75 additions & 0 deletions elements/jsonform/src/custom-inputs/spatial/utils.js
Original file line number Diff line number Diff line change
@@ -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;
};
Loading
Loading