import React from 'react' import PropTypes from 'prop-types' import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; import {latest} from '@mapbox/mapbox-gl-style-spec' import InputButton from './InputButton' import InputSpec from './InputSpec' import InputNumber from './InputNumber' import InputString from './InputString' import InputSelect from './InputSelect' import FieldDocLabel from './FieldDocLabel' import Block from './Block' import docUid from '../libs/document-uid' import sortNumerically from '../libs/sort-numerically' import {findDefaultFromSpec} from '../util/spec-helper'; import labelFromFieldName from './_labelFromFieldName' import DeleteStopButton from './_DeleteStopButton' function setStopRefs(props, state) { // This is initialsed below only if required to improved performance. let newRefs; if(props.value && props.value.stops) { props.value.stops.forEach((val, idx) => { if(!state.refs.hasOwnProperty(idx)) { if(!newRefs) { newRefs = {...state}; } newRefs[idx] = docUid("stop-"); } }) } return newRefs; } export default class DataProperty extends React.Component { static propTypes = { onChange: PropTypes.func, onDeleteStop: PropTypes.func, onAddStop: PropTypes.func, onExpressionClick: PropTypes.func, fieldName: PropTypes.string, fieldType: PropTypes.string, fieldSpec: PropTypes.object, value: PropTypes.oneOfType([ PropTypes.object, PropTypes.string, PropTypes.number, PropTypes.bool, PropTypes.array ]), errors: PropTypes.object, } state = { refs: {} } componentDidMount() { const newRefs = setStopRefs(this.props, this.state); if(newRefs) { this.setState({ refs: newRefs }) } } static getDerivedStateFromProps(props, state) { const newRefs = setStopRefs(props, state); if(newRefs) { return { refs: newRefs }; } return null; } getFieldFunctionType(fieldSpec) { if (fieldSpec.expression.interpolated) { return "exponential" } if (fieldSpec.type === "number") { return "interval" } return "categorical" } getDataFunctionTypes(fieldSpec) { 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) { 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 = {}; mappedWithRef .forEach((stop, idx) =>{ newRefs[idx] = stop.ref; }) this.setState({ refs: newRefs }); return mappedWithRef.map((item) => item.data); } onChange = (fieldName, value) => { 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)], [{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)] ], ...value, } } this.props.onChange(fieldName, value); } changeStop(changeIdx, stopData, value) { const stops = this.props.value.stops.slice(0) // const changedStop = stopData.zoom === undefined ? stopData.value : stopData stops[changeIdx] = [ { ...stopData, 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) { const changedValue = { ...this.props.value, base: newValue } if (changedValue.base === undefined) { delete changedValue["base"]; } this.props.onChange(this.props.fieldName, changedValue) } changeDataType(propVal) { if (propVal === "interpolate") { this.props.onChangeToZoomFunction(); } else { this.onChange(this.props.fieldName, { ...this.props.value, type: propVal, }); } } changeDataProperty(propName, propVal) { if (propVal) { this.props.value[propName] = propVal } else { delete this.props.value[propName] } this.onChange(this.props.fieldName, this.props.value) } render() { const {fieldName, fieldType, errors} = this.props; 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': "Input value", label: "Data value", value: dataLevel, onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, 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} />
} const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`; const foundErrors = Object.entries(errors).filter(([key, error]) => { return key.startsWith(errorKeyStart); }); const message = foundErrors.map(([key, error]) => { return error.message; }).join(""); const error = message ? {message} : undefined; return {zoomInput} {dataInput} this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)} /> {deleteStopBtn} }) } return
{labelFromFieldName(this.props.fieldName)}
this.changeDataType(propVal)} title={"Select a type of data scale (default is 'categorical')."} options={this.getDataFunctionTypes(this.props.fieldSpec)} />
{this.props.value.type !== "identity" &&
this.changeBase(newValue)} />
}
this.changeDataProperty("property", propVal)} />
{dataFields && this.changeDataProperty("default", propVal)} /> } {dataFields &&
{dataFields}
Stops
Zoom Input value Output value
}
{dataFields && Add stop } Convert to expression
} }