mirror of
https://github.com/maputnik/editor.git
synced 2025-12-09 07:40:01 +00:00
## Launch Checklist This PR adds back the error panel which was under the map for some reason. It also highlights problematic layers in the layers list (which already worked). It also highlights the field that has an error related to it. It fixes the error types throughout the code. Before: <img width="1141" height="665" alt="image" src="https://github.com/user-attachments/assets/c0593d6c-8f14-41b3-8a51-bc359446656d" /> After: <img width="1141" height="665" alt="image" src="https://github.com/user-attachments/assets/1ffeebb7-31ea-4ed5-97f4-fc5f907a6aea" /> - [x] Briefly describe the changes in this PR. - [x] Include before/after visuals or gifs if this PR includes visual changes. - [x] Write tests for all new functionality. - [x] Add an entry to `CHANGELOG.md` under the `## main` section. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
407 lines
10 KiB
TypeScript
407 lines
10 KiB
TypeScript
import React from "react";
|
|
|
|
import SpecProperty from "./_SpecProperty";
|
|
import DataProperty, { type Stop } from "./_DataProperty";
|
|
import ZoomProperty from "./_ZoomProperty";
|
|
import ExpressionProperty from "./_ExpressionProperty";
|
|
import {function as styleFunction} from "@maplibre/maplibre-gl-style-spec";
|
|
import {findDefaultFromSpec} from "../libs/spec-helper";
|
|
import { type MappedLayerErrors } from "../libs/definitions";
|
|
|
|
|
|
function isLiteralExpression(value: any) {
|
|
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
|
}
|
|
|
|
function isGetExpression(value: any) {
|
|
return (
|
|
Array.isArray(value) &&
|
|
value.length === 2 &&
|
|
value[0] === "get"
|
|
);
|
|
}
|
|
|
|
function isZoomField(value: any) {
|
|
return (
|
|
typeof(value) === "object" &&
|
|
value.stops &&
|
|
typeof(value.property) === "undefined" &&
|
|
Array.isArray(value.stops) &&
|
|
value.stops.length > 1 &&
|
|
value.stops.every((stop: Stop) => {
|
|
return (
|
|
Array.isArray(stop) &&
|
|
stop.length === 2
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
function isIdentityProperty(value: any) {
|
|
return (
|
|
typeof(value) === "object" &&
|
|
value.type === "identity" &&
|
|
Object.prototype.hasOwnProperty.call(value, "property")
|
|
);
|
|
}
|
|
|
|
function isDataStopProperty(value: any) {
|
|
return (
|
|
typeof(value) === "object" &&
|
|
value.stops &&
|
|
typeof(value.property) !== "undefined" &&
|
|
value.stops.length > 1 &&
|
|
Array.isArray(value.stops) &&
|
|
value.stops.every((stop: Stop) => {
|
|
return (
|
|
Array.isArray(stop) &&
|
|
stop.length === 2 &&
|
|
typeof(stop[0]) === "object"
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
function isDataField(value: any) {
|
|
return (
|
|
isIdentityProperty(value) ||
|
|
isDataStopProperty(value)
|
|
);
|
|
}
|
|
|
|
function isPrimative(value: any): value is string | boolean | number {
|
|
const valid = ["string", "boolean", "number"];
|
|
return valid.includes(typeof(value));
|
|
}
|
|
|
|
function isArrayOfPrimatives(values: any): values is Array<string | boolean | number> {
|
|
if (Array.isArray(values)) {
|
|
return values.every(isPrimative);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getDataType(value: any, fieldSpec={} as any) {
|
|
if (value === undefined) {
|
|
return "value";
|
|
}
|
|
else if (isPrimative(value)) {
|
|
return "value";
|
|
}
|
|
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
|
|
return "value";
|
|
}
|
|
else if (fieldSpec.type === "numberArray" && isArrayOfPrimatives(value)) {
|
|
return "value";
|
|
}
|
|
else if (fieldSpec.type === "colorArray") {
|
|
return "value";
|
|
}
|
|
else if (fieldSpec.type === "padding") {
|
|
return "value";
|
|
}
|
|
else if (fieldSpec.type === "variableAnchorOffsetCollection") {
|
|
return "value";
|
|
}
|
|
else if (isZoomField(value)) {
|
|
return "zoom_function";
|
|
}
|
|
else if (isDataField(value)) {
|
|
return "data_function";
|
|
}
|
|
else {
|
|
return "expression";
|
|
}
|
|
}
|
|
|
|
|
|
type FieldFunctionProps = {
|
|
onChange(fieldName: string, value: any): unknown
|
|
fieldName: string
|
|
fieldType: string
|
|
fieldSpec: any
|
|
errors?: MappedLayerErrors
|
|
value?: any
|
|
};
|
|
|
|
|
|
/** Supports displaying spec field for zoom function objects
|
|
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
|
*/
|
|
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
|
const [dataType, setDataType] = React.useState(
|
|
getDataType(props.value, props.fieldSpec)
|
|
);
|
|
const [isEditing, setIsEditing] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
if (!isEditing) {
|
|
setDataType(getDataType(props.value, props.fieldSpec));
|
|
}
|
|
}, [props.value, props.fieldSpec, isEditing]);
|
|
|
|
const getFieldFunctionType = (fieldSpec: any) => {
|
|
if (fieldSpec.expression.interpolated) {
|
|
return "exponential";
|
|
}
|
|
if (fieldSpec.type === "number") {
|
|
return "interval";
|
|
}
|
|
return "categorical";
|
|
};
|
|
|
|
const addStop = () => {
|
|
const stops = props.value.stops.slice(0);
|
|
const lastStop = stops[stops.length - 1];
|
|
if (typeof lastStop[0] === "object") {
|
|
stops.push([
|
|
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
|
|
lastStop[1],
|
|
]);
|
|
} else {
|
|
stops.push([lastStop[0] + 1, lastStop[1]]);
|
|
}
|
|
|
|
const changedValue = {
|
|
...props.value,
|
|
stops: stops,
|
|
};
|
|
|
|
props.onChange(props.fieldName, changedValue);
|
|
};
|
|
|
|
const deleteExpression = () => {
|
|
const { fieldSpec, fieldName } = props;
|
|
props.onChange(fieldName, fieldSpec.default);
|
|
setDataType("value");
|
|
};
|
|
|
|
const deleteStop = (stopIdx: number) => {
|
|
const stops = props.value.stops.slice(0);
|
|
stops.splice(stopIdx, 1);
|
|
|
|
let changedValue: any = {
|
|
...props.value,
|
|
stops: stops,
|
|
};
|
|
|
|
if (stops.length === 1) {
|
|
changedValue = stops[0][1];
|
|
}
|
|
|
|
props.onChange(props.fieldName, changedValue);
|
|
};
|
|
|
|
const makeZoomFunction = () => {
|
|
const { value } = props;
|
|
|
|
let zoomFunc: any;
|
|
if (typeof value === "object") {
|
|
if (value.stops) {
|
|
zoomFunc = {
|
|
base: value.base,
|
|
stops: value.stops.map((stop: Stop) => {
|
|
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
|
}),
|
|
};
|
|
} else {
|
|
zoomFunc = {
|
|
base: value.base,
|
|
stops: [
|
|
[6, findDefaultFromSpec(props.fieldSpec)],
|
|
[10, findDefaultFromSpec(props.fieldSpec)],
|
|
],
|
|
};
|
|
}
|
|
} else {
|
|
zoomFunc = {
|
|
stops: [
|
|
[6, value || findDefaultFromSpec(props.fieldSpec)],
|
|
[10, value || findDefaultFromSpec(props.fieldSpec)],
|
|
],
|
|
};
|
|
}
|
|
|
|
props.onChange(props.fieldName, zoomFunc);
|
|
};
|
|
|
|
const undoExpression = () => {
|
|
const { value, fieldName } = props;
|
|
|
|
if (isGetExpression(value)) {
|
|
props.onChange(fieldName, {
|
|
type: "identity",
|
|
property: value[1],
|
|
});
|
|
setDataType("value");
|
|
} else if (isLiteralExpression(value)) {
|
|
props.onChange(fieldName, value[1]);
|
|
setDataType("value");
|
|
}
|
|
};
|
|
|
|
const canUndo = () => {
|
|
const { value, fieldSpec } = props;
|
|
return (
|
|
isGetExpression(value) ||
|
|
isLiteralExpression(value) ||
|
|
isPrimative(value) ||
|
|
(Array.isArray(value) && fieldSpec.type === "array")
|
|
);
|
|
};
|
|
|
|
const makeExpression = () => {
|
|
const { value, fieldSpec } = props;
|
|
let expression;
|
|
|
|
if (typeof value === "object" && "stops" in value) {
|
|
expression = styleFunction.convertFunction(value, fieldSpec);
|
|
} else if (isIdentityProperty(value)) {
|
|
expression = ["get", value.property];
|
|
} else {
|
|
expression = ["literal", value || props.fieldSpec.default];
|
|
}
|
|
props.onChange(props.fieldName, expression);
|
|
};
|
|
|
|
const makeDataFunction = () => {
|
|
const functionType = getFieldFunctionType(props.fieldSpec);
|
|
const stopValue = functionType === "categorical" ? "" : 0;
|
|
const { value } = props;
|
|
let dataFunc;
|
|
|
|
if (typeof value === "object") {
|
|
if (value.stops) {
|
|
dataFunc = {
|
|
property: "",
|
|
type: functionType,
|
|
base: value.base,
|
|
stops: value.stops.map((stop: Stop) => {
|
|
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
|
}),
|
|
};
|
|
} else {
|
|
dataFunc = {
|
|
property: "",
|
|
type: functionType,
|
|
base: value.base,
|
|
stops: [
|
|
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
|
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
|
],
|
|
};
|
|
}
|
|
} else {
|
|
dataFunc = {
|
|
property: "",
|
|
type: functionType,
|
|
base: value.base,
|
|
stops: [
|
|
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
|
[{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
|
],
|
|
};
|
|
}
|
|
|
|
props.onChange(props.fieldName, dataFunc);
|
|
};
|
|
|
|
const makeElevationFunction = () => {
|
|
const expression = [
|
|
"interpolate",
|
|
["linear"],
|
|
["elevation"],
|
|
0,
|
|
"black",
|
|
2000,
|
|
"white"
|
|
];
|
|
|
|
props.onChange(props.fieldName, expression);
|
|
};
|
|
|
|
const onMarkEditing = () => {
|
|
setIsEditing(true);
|
|
};
|
|
|
|
const onUnmarkEditing = () => {
|
|
setIsEditing(false);
|
|
};
|
|
|
|
const propClass =
|
|
props.fieldSpec.default === props.value ? "maputnik-default-property" : "maputnik-modified-property";
|
|
|
|
let specField;
|
|
|
|
if (dataType === "expression") {
|
|
specField = (
|
|
<ExpressionProperty
|
|
errors={props.errors}
|
|
onChange={props.onChange.bind(null, props.fieldName)}
|
|
canUndo={canUndo}
|
|
onUndo={undoExpression}
|
|
onDelete={deleteExpression}
|
|
fieldType={props.fieldType}
|
|
fieldName={props.fieldName}
|
|
fieldSpec={props.fieldSpec}
|
|
value={props.value}
|
|
onFocus={onMarkEditing}
|
|
onBlur={onUnmarkEditing}
|
|
/>
|
|
);
|
|
} else if (dataType === "zoom_function") {
|
|
specField = (
|
|
<ZoomProperty
|
|
errors={props.errors}
|
|
onChange={props.onChange.bind(null)}
|
|
fieldType={props.fieldType}
|
|
fieldName={props.fieldName}
|
|
fieldSpec={props.fieldSpec}
|
|
value={props.value}
|
|
onDeleteStop={deleteStop}
|
|
onAddStop={addStop}
|
|
onChangeToDataFunction={makeDataFunction}
|
|
onExpressionClick={makeExpression}
|
|
/>
|
|
);
|
|
} else if (dataType === "data_function") {
|
|
specField = (
|
|
<DataProperty
|
|
errors={props.errors}
|
|
onChange={props.onChange.bind(null)}
|
|
fieldType={props.fieldType}
|
|
fieldName={props.fieldName}
|
|
fieldSpec={props.fieldSpec}
|
|
value={props.value}
|
|
onDeleteStop={deleteStop}
|
|
onAddStop={addStop}
|
|
onChangeToZoomFunction={makeZoomFunction}
|
|
onExpressionClick={makeExpression}
|
|
/>
|
|
);
|
|
} else {
|
|
specField = (
|
|
<SpecProperty
|
|
errors={props.errors}
|
|
onChange={props.onChange.bind(null)}
|
|
fieldType={props.fieldType}
|
|
fieldName={props.fieldName}
|
|
fieldSpec={props.fieldSpec}
|
|
value={props.value}
|
|
onZoomClick={makeZoomFunction}
|
|
onDataClick={makeDataFunction}
|
|
onExpressionClick={makeExpression}
|
|
onElevationClick={makeElevationFunction}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={propClass} data-wd-key={"spec-field-container:" + props.fieldName}>
|
|
{specField}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FieldFunction;
|