Merge pull request #645 from orangemug/feature/add-support-for-identity-functions

Add support for identity functions
This commit is contained in:
Orange Mug 2020-04-14 09:12:35 +01:00 committed by GitHub
commit d98637cb12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 195 additions and 111 deletions

View file

@ -6,12 +6,21 @@ import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty' import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty' import ExpressionProperty from './_ExpressionProperty'
import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec'; import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec';
import {findDefaultFromSpec} from '../util/spec-helper';
function isLiteralExpression (value) { function isLiteralExpression (value) {
return (Array.isArray(value) && value.length === 2 && value[0] === "literal"); return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
} }
function isGetExpression (value) {
return (
Array.isArray(value) &&
value.length === 2 &&
value[0] === "get"
);
}
function isZoomField(value) { function isZoomField(value) {
return ( return (
typeof(value) === 'object' && typeof(value) === 'object' &&
@ -28,7 +37,15 @@ function isZoomField(value) {
); );
} }
function isDataField(value) { function isIdentityProperty (value) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
value.hasOwnProperty("property")
);
}
function isDataStopProperty (value) {
return ( return (
typeof(value) === 'object' && typeof(value) === 'object' &&
value.stops && value.stops &&
@ -45,6 +62,13 @@ function isDataField(value) {
); );
} }
function isDataField(value) {
return (
isIdentityProperty(value) ||
isDataStopProperty(value)
);
}
function isPrimative (value) { function isPrimative (value) {
const valid = ["string", "boolean", "number"]; const valid = ["string", "boolean", "number"];
return valid.includes(typeof(value)); return valid.includes(typeof(value));
@ -78,24 +102,6 @@ function getDataType (value, fieldSpec={}) {
} }
} }
/**
* If we don't have a default value just make one up
*/
function findDefaultFromSpec (spec) {
if (spec.hasOwnProperty('default')) {
return spec.default;
}
const defaults = {
'color': '#000000',
'string': '',
'boolean': false,
'number': 0,
'array': [],
}
return defaults[spec.type] || '';
}
/** Supports displaying spec field for zoom function objects /** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
@ -206,7 +212,16 @@ export default class FunctionSpecProperty extends React.Component {
undoExpression = () => { undoExpression = () => {
const {value, fieldName} = this.props; const {value, fieldName} = this.props;
if (isLiteralExpression(value)) { if (isGetExpression(value)) {
this.props.onChange(fieldName, {
"type": "identity",
"property": value[1]
});
this.setState({
dataType: "value",
});
}
else if (isLiteralExpression(value)) {
this.props.onChange(fieldName, value[1]); this.props.onChange(fieldName, value[1]);
this.setState({ this.setState({
dataType: "value", dataType: "value",
@ -217,6 +232,7 @@ export default class FunctionSpecProperty extends React.Component {
canUndo = () => { canUndo = () => {
const {value, fieldSpec} = this.props; const {value, fieldSpec} = this.props;
return ( return (
isGetExpression(value) ||
isLiteralExpression(value) || isLiteralExpression(value) ||
isPrimative(value) || isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === "array") (Array.isArray(value) && fieldSpec.type === "array")
@ -230,6 +246,9 @@ export default class FunctionSpecProperty extends React.Component {
if (typeof(value) === "object" && 'stops' in value) { if (typeof(value) === "object" && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec); expression = styleFunction.convertFunction(value, fieldSpec);
} }
else if (isIdentityProperty(value)) {
expression = ["get", value.property];
}
else { else {
expression = ["literal", value || this.props.fieldSpec.default]; expression = ["literal", value || this.props.fieldSpec.default];
} }

View file

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import Button from '../Button' import Button from '../Button'
import SpecField from './SpecField' import SpecField from './SpecField'
@ -10,6 +11,7 @@ import DocLabel from './DocLabel'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import docUid from '../../libs/document-uid' import docUid from '../../libs/document-uid'
import sortNumerically from '../../libs/sort-numerically' import sortNumerically from '../../libs/sort-numerically'
import {findDefaultFromSpec} from '../util/spec-helper';
import labelFromFieldName from './_labelFromFieldName' import labelFromFieldName from './_labelFromFieldName'
import DeleteStopButton from './_DeleteStopButton' import DeleteStopButton from './_DeleteStopButton'
@ -89,10 +91,10 @@ export default class DataProperty extends React.Component {
getDataFunctionTypes(fieldSpec) { getDataFunctionTypes(fieldSpec) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return ["categorical", "interval", "exponential"] return ["categorical", "interval", "exponential", "identity"]
} }
else { else {
return ["categorical", "interval"] return ["categorical", "interval", "identity"]
} }
} }
@ -122,6 +124,29 @@ export default class DataProperty extends React.Component {
return mappedWithRef.map((item) => item.data); 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) { changeStop(changeIdx, stopData, value) {
const stops = this.props.value.stops.slice(0) const stops = this.props.value.stops.slice(0)
const changedStop = stopData.zoom === undefined ? stopData.value : stopData const changedStop = stopData.zoom === undefined ? stopData.value : stopData
@ -133,7 +158,7 @@ export default class DataProperty extends React.Component {
...this.props.value, ...this.props.value,
stops: orderedStops, stops: orderedStops,
} }
this.props.onChange(this.props.fieldName, changedValue) this.onChange(this.props.fieldName, changedValue)
} }
changeDataProperty(propName, propVal) { changeDataProperty(propName, propVal) {
@ -143,7 +168,7 @@ export default class DataProperty extends React.Component {
else { else {
delete this.props.value[propName] delete this.props.value[propName]
} }
this.props.onChange(this.props.fieldName, this.props.value) this.onChange(this.props.fieldName, this.props.value)
} }
render() { render() {
@ -153,69 +178,72 @@ export default class DataProperty extends React.Component {
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec) this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
} }
const dataFields = this.props.value.stops.map((stop, idx) => { let dataFields;
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined; if (this.props.value.stops) {
const key = this.state.refs[idx]; dataFields = this.props.value.stops.map((stop, idx) => {
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0]; const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
const value = stop[1] const key = this.state.refs[idx];
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} /> const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
const value = stop[1]
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
const dataProps = { const dataProps = {
label: "Data value", label: "Data value",
value: dataLevel, value: dataLevel,
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value) onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
} }
let dataInput; let dataInput;
if(this.props.value.type === "categorical") { if(this.props.value.type === "categorical") {
dataInput = <StringInput {...dataProps} /> dataInput = <StringInput {...dataProps} />
} }
else { else {
dataInput = <NumberInput {...dataProps} /> dataInput = <NumberInput {...dataProps} />
} }
let zoomInput = null; let zoomInput = null;
if(zoomLevel !== undefined) { if(zoomLevel !== undefined) {
zoomInput = <div className="maputnik-data-spec-property-stop-edit"> zoomInput = <div className="maputnik-data-spec-property-stop-edit">
<NumberInput <NumberInput
value={zoomLevel} value={zoomLevel}
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
min={0} min={0}
max={22} max={22}
/> />
</div> </div>
} }
const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`; const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
const foundErrors = Object.entries(errors).filter(([key, error]) => { const foundErrors = Object.entries(errors).filter(([key, error]) => {
return key.startsWith(errorKeyStart); return key.startsWith(errorKeyStart);
}); });
const message = foundErrors.map(([key, error]) => { const message = foundErrors.map(([key, error]) => {
return error.message; return error.message;
}).join(""); }).join("");
const error = message ? {message} : undefined; const error = message ? {message} : undefined;
return <InputBlock return <InputBlock
error={error} error={error}
key={key} key={key}
action={deleteStopBtn} action={deleteStopBtn}
label="" label=""
> >
{zoomInput} {zoomInput}
<div className="maputnik-data-spec-property-stop-data"> <div className="maputnik-data-spec-property-stop-data">
{dataInput} {dataInput}
</div> </div>
<div className="maputnik-data-spec-property-stop-value"> <div className="maputnik-data-spec-property-stop-value">
<SpecField <SpecField
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={value} value={value}
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)} onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
/> />
</div> </div>
</InputBlock> </InputBlock>
}) })
}
return <div className="maputnik-data-spec-block"> return <div className="maputnik-data-spec-block">
<div className="maputnik-data-spec-property"> <div className="maputnik-data-spec-property">
@ -223,18 +251,6 @@ export default class DataProperty extends React.Component {
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)} label={labelFromFieldName(this.props.fieldName)}
> >
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Property"
/>
<div className="maputnik-data-spec-property-input">
<StringInput
value={this.props.value.property}
title={"Input a data property to base styles off of."}
onChange={propVal => this.changeDataProperty("property", propVal)}
/>
</div>
</div>
<div className="maputnik-data-spec-property-group"> <div className="maputnik-data-spec-property-group">
<DocLabel <DocLabel
label="Type" label="Type"
@ -250,31 +266,53 @@ export default class DataProperty extends React.Component {
</div> </div>
<div className="maputnik-data-spec-property-group"> <div className="maputnik-data-spec-property-group">
<DocLabel <DocLabel
label="Default" label="Property"
/> />
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<SpecField <StringInput
fieldName={this.props.fieldName} value={this.props.value.property}
fieldSpec={this.props.fieldSpec} title={"Input a data property to base styles off of."}
value={this.props.value.default} onChange={propVal => this.changeDataProperty("property", propVal)}
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
/> />
</div> </div>
</div> </div>
{dataFields &&
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Default"
/>
<div className="maputnik-data-spec-property-input">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value.default}
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
/>
</div>
</div>
}
</InputBlock> </InputBlock>
</div> </div>
{dataFields} {dataFields &&
<Button <>
className="maputnik-add-stop" {dataFields}
onClick={this.props.onAddStop.bind(this)} <Button
> className="maputnik-add-stop"
Add stop onClick={this.props.onAddStop.bind(this)}
</Button> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</Button>
</>
}
<Button <Button
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onExpressionClick.bind(this)} onClick={this.props.onExpressionClick.bind(this)}
> >
Convert to expression <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</Button> </Button>
</div> </div>
} }

View file

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import Button from '../Button' import Button from '../Button'
import SpecField from './SpecField' import SpecField from './SpecField'
@ -176,13 +177,17 @@ export default class ZoomProperty extends React.Component {
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)} onClick={this.props.onAddStop.bind(this)}
> >
Add stop <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</Button> </Button>
<Button <Button
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onExpressionClick.bind(this)} onClick={this.props.onExpressionClick.bind(this)}
> >
Convert to expression <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</Button> </Button>
</div> </div>
} }

View file

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { combiningFilterOps } from '../../libs/filterops.js' import { combiningFilterOps } from '../../libs/filterops.js'
import {mdiTableRowPlusAfter} from '@mdi/js';
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec' import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
import DocLabel from '../fields/DocLabel' import DocLabel from '../fields/DocLabel'
@ -261,8 +262,11 @@ export default class CombiningFilterEditor extends React.Component {
<Button <Button
data-wd-key="layer-filter-button" data-wd-key="layer-filter-button"
className="maputnik-add-filter" className="maputnik-add-filter"
onClick={this.addFilterItem}> onClick={this.addFilterItem}
Add filter >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add filter
</Button> </Button>
</div> </div>
<div <div

View file

@ -0,0 +1,18 @@
/**
* If we don't have a default value just make one up
*/
export function findDefaultFromSpec (spec) {
if (spec.hasOwnProperty('default')) {
return spec.default;
}
const defaults = {
'color': '#000000',
'string': '',
'boolean': false,
'number': 0,
'array': [],
}
return defaults[spec.type] || '';
}