import React from "react"; import {PiListPlusBold} from "react-icons/pi"; import {TbMathFunction} from "react-icons/tb"; import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json"; import InputButton from "./InputButton"; import InputSpec from "./InputSpec"; import InputNumber from "./InputNumber"; import InputString from "./InputString"; import InputSelect from "./InputSelect"; import Block from "./Block"; import docUid from "../libs/document-uid"; import sortNumerically from "../libs/sort-numerically"; import {findDefaultFromSpec} from "../libs/spec-helper"; import { type WithTranslation, withTranslation } from "react-i18next"; import labelFromFieldName from "../libs/label-from-field-name"; import DeleteStopButton from "./_DeleteStopButton"; import { type MappedLayerErrors } from "../libs/definitions"; function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState) { // This is initialsed below only if required to improved performance. let newRefs: {[key: number]: string} | undefined; if(props.value && props.value.stops) { props.value.stops.forEach((_val, idx) => { if(!Object.prototype.hasOwnProperty.call(state.refs, idx)) { if(!newRefs) { newRefs = {...state}; } newRefs[idx] = docUid("stop-"); } }); } return newRefs; } type DataPropertyInternalProps = { onChange?(fieldName: string, value: any): unknown onDeleteStop?(...args: unknown[]): unknown onAddStop?(...args: unknown[]): unknown onExpressionClick?(...args: unknown[]): unknown onChangeToZoomFunction?(...args: unknown[]): unknown fieldName: string fieldType?: string fieldSpec?: object value?: DataPropertyValue errors?: MappedLayerErrors } & WithTranslation; type DataPropertyState = { refs: {[key: number]: string} }; type DataPropertyValue = { default?: any property?: string base?: number type?: string stops: Stop[] }; export type Stop = [{ zoom: number value: number }, number]; class DataPropertyInternal extends React.Component { state = { refs: {} as {[key: number]: string} }; componentDidMount() { const newRefs = setStopRefs(this.props, this.state); if(newRefs) { this.setState({ refs: newRefs }); } } static getDerivedStateFromProps(props: Readonly, state: DataPropertyState) { const newRefs = setStopRefs(props, state); if(newRefs) { return { refs: newRefs }; } return null; } getFieldFunctionType(fieldSpec: any) { if (fieldSpec.expression.interpolated) { return "exponential"; } if (fieldSpec.type === "number") { return "interval"; } return "categorical"; } getDataFunctionTypes(fieldSpec: any) { if (fieldSpec.expression.interpolated) { return ["interpolate", "categorical", "interval", "exponential", "identity"]; } else { return ["categorical", "interval", "identity"]; } } // Order the stops altering the refs to reflect their new position. orderStopsByZoom(stops: Stop[]) { const mappedWithRef = stops .map((stop, idx) => { return { ref: this.state.refs[idx], data: stop }; }) // Sort by zoom .sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom)); // Fetch the new position of the stops const newRefs = {} as {[key: number]: string}; mappedWithRef .forEach((stop, idx) =>{ newRefs[idx] = stop.ref; }); this.setState({ refs: newRefs }); return mappedWithRef.map((item) => item.data); } onChange = (fieldName: string, value: any) => { if (value.type === "identity") { value = { type: value.type, property: value.property, }; } else { const stopValue = value.type === "categorical" ? "" : 0; value = { property: "", type: value.type, // Default props if they don't already exist. stops: [ [{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)], [{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)] ], ...value, }; } this.props.onChange!(fieldName, value); }; changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) { const stops = this.props.value?.stops.slice(0) || []; // const changedStop = stopData.zoom === undefined ? stopData.value : stopData stops[changeIdx] = [ { value: stopData.value, zoom: (stopData.zoom === undefined) ? 0 : stopData.zoom, }, value ]; const orderedStops = this.orderStopsByZoom(stops); const changedValue = { ...this.props.value, stops: orderedStops, }; this.onChange(this.props.fieldName, changedValue); } changeBase(newValue: number | undefined) { const changedValue = { ...this.props.value, base: newValue }; if (changedValue.base === undefined) { delete changedValue["base"]; } this.props.onChange!(this.props.fieldName, changedValue); } changeDataType(propVal: string) { if (propVal === "interpolate" && this.props.onChangeToZoomFunction) { this.props.onChangeToZoomFunction(); } else { this.onChange(this.props.fieldName, { ...this.props.value, type: propVal, }); } } changeDataProperty(propName: "property" | "default", propVal: any) { if (propVal) { this.props.value![propName] = propVal; } else { delete this.props.value![propName]; } this.onChange(this.props.fieldName, this.props.value); } render() { const t = this.props.t; if (typeof this.props.value?.type === "undefined") { this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec); } let dataFields; if (this.props.value?.stops) { dataFields = this.props.value.stops.map((stop, idx) => { const zoomLevel = typeof stop[0] === "object" ? stop[0].zoom : undefined; const key = this.state.refs[idx]; const dataLevel = typeof stop[0] === "object" ? stop[0].value : stop[0]; const value = stop[1]; const deleteStopBtn = ; const dataProps = { "aria-label": t("Input value"), label: t("Data value"), value: dataLevel as any, onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value) }; let dataInput; if(this.props.value?.type === "categorical") { dataInput = ; } else { dataInput = ; } let zoomInput = null; if(zoomLevel !== undefined) { zoomInput =
this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} min={0} max={22} />
; } return {zoomInput} {dataInput} this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue as number)} /> {deleteStopBtn} ; }); } return
{labelFromFieldName(this.props.fieldName)}
this.changeDataType(propVal)} title={t("Select a type of data scale (default is 'categorical').")} options={this.getDataFunctionTypes(this.props.fieldSpec)} />
{this.props.value?.type !== "identity" &&
this.changeBase(newValue as number)} />
}
this.changeDataProperty("property", propVal)} />
{dataFields && this.changeDataProperty("default", propVal)} /> } {dataFields &&
{dataFields}
{t("Stops")}
{t("Zoom")} {t("Input value")} {t("Output value")}
}
{dataFields && {t("Add stop")} } {t("Convert to expression")}
; } } const DataProperty = withTranslation()(DataPropertyInternal); export default DataProperty;