mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2024-12-27 09:05:25 +01:00
Added initial expression work and UI errors.
This commit is contained in:
parent
63ed8c1de3
commit
725b752e35
25 changed files with 360 additions and 53 deletions
|
@ -207,6 +207,7 @@ export default class App extends React.Component {
|
|||
errors: [],
|
||||
infos: [],
|
||||
mapStyle: style.emptyStyle,
|
||||
hopefulMapStyle: style.emptyStyle,
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
|
@ -279,7 +280,7 @@ export default class App extends React.Component {
|
|||
}
|
||||
|
||||
updateFonts(urlTemplate) {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const metadata = this.state.hopefulMapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
|
||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
|
@ -318,24 +319,50 @@ export default class App extends React.Component {
|
|||
onStyleChanged = (newStyle, save=true) => {
|
||||
|
||||
const errors = validate(newStyle, latest)
|
||||
const mappedErrors = errors.map(error => {
|
||||
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
||||
if (layerMatch) {
|
||||
const [matchStr, index, group, property, message] = layerMatch;
|
||||
const key = (group && property) ? [group, property].join(".") : property;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
type: "layer",
|
||||
data: {
|
||||
index,
|
||||
key,
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
if(errors.length === 0) {
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
if(newStyle.glyphs !== this.state.hopefulMapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs)
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
if(newStyle.sprite !== this.state.hopefulMapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite)
|
||||
}
|
||||
|
||||
this.revisionStore.addRevision(newStyle)
|
||||
if(save) this.saveStyle(newStyle)
|
||||
this.setState({
|
||||
hopefulMapStyle: newStyle,
|
||||
mapStyle: newStyle,
|
||||
errors: [],
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
errors: errors.map(err => err.message)
|
||||
hopefulMapStyle: newStyle,
|
||||
errors: mappedErrors,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -344,7 +371,7 @@ export default class App extends React.Component {
|
|||
|
||||
onUndo = () => {
|
||||
const activeStyle = this.revisionStore.undo()
|
||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
||||
const messages = undoMessages(this.state.hopefulMapStyle, activeStyle)
|
||||
this.saveStyle(activeStyle)
|
||||
this.setState({
|
||||
mapStyle: activeStyle,
|
||||
|
@ -354,7 +381,7 @@ export default class App extends React.Component {
|
|||
|
||||
onRedo = () => {
|
||||
const activeStyle = this.revisionStore.redo()
|
||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
||||
const messages = redoMessages(this.state.hopefulMapStyle, activeStyle)
|
||||
this.saveStyle(activeStyle)
|
||||
this.setState({
|
||||
mapStyle: activeStyle,
|
||||
|
@ -364,7 +391,7 @@ export default class App extends React.Component {
|
|||
|
||||
onMoveLayer = (move) => {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
let layers = this.state.hopefulMapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
newIndex = clamp(newIndex, 0, layers.length-1);
|
||||
if(oldIndex === newIndex) return;
|
||||
|
@ -382,14 +409,14 @@ export default class App extends React.Component {
|
|||
|
||||
onLayersChange = (changedLayers) => {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
...this.state.hopefulMapStyle,
|
||||
layers: changedLayers
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerDestroy = (layerId) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
let layers = this.state.hopefulMapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
|
@ -408,7 +435,7 @@ export default class App extends React.Component {
|
|||
}
|
||||
|
||||
onLayerVisibilityToggle = (layerId) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
let layers = this.state.hopefulMapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
|
@ -423,7 +450,7 @@ export default class App extends React.Component {
|
|||
|
||||
|
||||
onLayerIdChange = (oldId, newId) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const changedLayers = this.state.hopefulMapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, oldId)
|
||||
|
||||
changedLayers[idx] = {
|
||||
|
@ -435,7 +462,8 @@ export default class App extends React.Component {
|
|||
}
|
||||
|
||||
onLayerChanged = (layer) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
console.log("test: onLayerChanged", layer);
|
||||
const changedLayers = this.state.hopefulMapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layer.id)
|
||||
changedLayers[idx] = layer
|
||||
|
||||
|
@ -472,7 +500,7 @@ export default class App extends React.Component {
|
|||
fetchSources() {
|
||||
const sourceList = {...this.state.sources};
|
||||
|
||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
for(let [key, val] of Object.entries(this.state.hopefulMapStyle.sources)) {
|
||||
if(sourceList.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -596,7 +624,7 @@ export default class App extends React.Component {
|
|||
}
|
||||
|
||||
onLayerSelect = (layerId) => {
|
||||
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
||||
const idx = style.indexOfLayer(this.state.hopefulMapStyle.layers, layerId)
|
||||
this.setState({ selectedLayerIndex: idx })
|
||||
}
|
||||
|
||||
|
@ -636,14 +664,14 @@ export default class App extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const layers = this.state.hopefulMapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const metadata = this.state.hopefulMapStyle.metadata || {}
|
||||
|
||||
const toolbar = <Toolbar
|
||||
renderer={this._getRenderer()}
|
||||
mapState={this.state.mapState}
|
||||
mapStyle={this.state.mapStyle}
|
||||
mapStyle={this.state.hopefulMapStyle}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
|
@ -662,6 +690,7 @@ export default class App extends React.Component {
|
|||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
errors={this.state.errors}
|
||||
/>
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
|
@ -669,7 +698,7 @@ export default class App extends React.Component {
|
|||
layer={selectedLayer}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.hopefulMapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
|
@ -679,9 +708,11 @@ export default class App extends React.Component {
|
|||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayerIdChange={this.onLayerIdChange}
|
||||
errors={this.state.errors}
|
||||
/> : null
|
||||
|
||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||
mapStyle={this.state.mapStyle}
|
||||
errors={this.state.errors}
|
||||
infos={this.state.infos}
|
||||
/> : null
|
||||
|
@ -704,7 +735,7 @@ export default class App extends React.Component {
|
|||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
/>
|
||||
<SettingsModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
mapStyle={this.state.hopefulMapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
|
@ -712,7 +743,7 @@ export default class App extends React.Component {
|
|||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
mapStyle={this.state.hopefulMapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
|
@ -723,7 +754,7 @@ export default class App extends React.Component {
|
|||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
/>
|
||||
<SourcesModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
mapStyle={this.state.hopefulMapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
|
|
|
@ -8,8 +8,23 @@ class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const errors = this.props.errors.map((m, i) => {
|
||||
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
|
||||
const errors = this.props.errors.map((error, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
const {parsed} = error;
|
||||
const {mapStyle} = this.props;
|
||||
content = (
|
||||
<>
|
||||
Layer <span>'{mapStyle.layers[parsed.data.index].id}'</span>: {parsed.data.message}
|
||||
</>
|
||||
);
|
||||
}
|
||||
else {
|
||||
content = error.message;
|
||||
}
|
||||
return <p key={"error-"+idx} className="maputnik-message-panel-error">
|
||||
{content}
|
||||
</p>
|
||||
})
|
||||
|
||||
const infos = this.props.infos.map((m, i) => {
|
||||
|
|
|
@ -4,6 +4,9 @@ import PropTypes from 'prop-types'
|
|||
import SpecProperty from './_SpecProperty'
|
||||
import DataProperty from './_DataProperty'
|
||||
import ZoomProperty from './_ZoomProperty'
|
||||
import ExpressionProperty from './_ExpressionProperty'
|
||||
import {expression} from '@mapbox/mapbox-gl-style-spec'
|
||||
|
||||
|
||||
|
||||
function isZoomField(value) {
|
||||
|
@ -14,6 +17,24 @@ function isDataField(value) {
|
|||
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
|
||||
}
|
||||
|
||||
/**
|
||||
* So here we can't just check is `Array.isArray(value)` because certain
|
||||
* properties accept arrays as values, for example `text-font`. So we must try
|
||||
* and create an expression.
|
||||
*/
|
||||
function isExpression(value, fieldSpec={}) {
|
||||
if (!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const out = expression.createExpression(value, fieldSpec);
|
||||
return (out.result === "success");
|
||||
}
|
||||
catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we don't have a default value just make one up
|
||||
*/
|
||||
|
@ -108,6 +129,11 @@ export default class FunctionSpecProperty extends React.Component {
|
|||
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||
}
|
||||
|
||||
makeExpression = () => {
|
||||
const expression = ["literal", this.props.value || this.props.fieldSpec.default];
|
||||
this.props.onChange(this.props.fieldName, expression);
|
||||
}
|
||||
|
||||
makeDataFunction = () => {
|
||||
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||
|
@ -126,9 +152,21 @@ export default class FunctionSpecProperty extends React.Component {
|
|||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||
let specField;
|
||||
|
||||
if (isZoomField(this.props.value)) {
|
||||
if (isExpression(this.props.value, this.props.fieldSpec)) {
|
||||
specField = (
|
||||
<ExpressionProperty
|
||||
error={this.props.error}
|
||||
onChange={this.props.onChange.bind(this, this.props.fieldName)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else if (isZoomField(this.props.value)) {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
error={this.props.error}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
|
@ -141,6 +179,7 @@ export default class FunctionSpecProperty extends React.Component {
|
|||
else if (isDataField(this.props.value)) {
|
||||
specField = (
|
||||
<DataProperty
|
||||
error={this.props.error}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
|
@ -153,12 +192,14 @@ export default class FunctionSpecProperty extends React.Component {
|
|||
else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
error={this.props.error}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onZoomClick={this.makeZoomFunction}
|
||||
onDataClick={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,14 +48,18 @@ export default class PropertyGroup extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {errors} = this.props;
|
||||
const fields = this.props.groupFields.map(fieldName => {
|
||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
||||
|
||||
const paint = this.props.layer.paint || {}
|
||||
const layout = this.props.layer.layout || {}
|
||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||
const fieldType = fieldName in paint ? 'paint' : 'layout';
|
||||
const errorKey = fieldType+"."+fieldName;
|
||||
|
||||
return <FunctionSpecField
|
||||
error={errors[errorKey]}
|
||||
onChange={this.onPropertyChange}
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
|
|
|
@ -200,6 +200,7 @@ export default class DataProperty extends React.Component {
|
|||
return <div className="maputnik-data-spec-block">
|
||||
<div className="maputnik-data-spec-property">
|
||||
<InputBlock
|
||||
error={this.props.error}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
>
|
||||
|
|
87
src/components/fields/_ExpressionProperty.jsx
Normal file
87
src/components/fields/_ExpressionProperty.jsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import Button from '../Button'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
import labelFromFieldName from './_labelFromFieldName'
|
||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||
|
||||
|
||||
function isLiteralExpression (value) {
|
||||
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
||||
}
|
||||
|
||||
export default class ExpressionProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onDeleteStop: PropTypes.func,
|
||||
fieldName: PropTypes.string,
|
||||
fieldSpec: PropTypes.object
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super();
|
||||
this.state = {
|
||||
lastValue: props.value,
|
||||
};
|
||||
}
|
||||
|
||||
onChange = (value) => {
|
||||
try {
|
||||
const jsonVal = JSON.parse(value);
|
||||
|
||||
if (isLiteralExpression(jsonVal)) {
|
||||
this.setState({
|
||||
lastValue: jsonVal
|
||||
});
|
||||
}
|
||||
|
||||
this.props.onChange(jsonVal);
|
||||
}
|
||||
catch (err) {
|
||||
// TODO: Handle JSON parse error
|
||||
}
|
||||
}
|
||||
|
||||
onDelete = () => {
|
||||
const {lastValue} = this.state;
|
||||
const {value, fieldName, fieldSpec} = this.props;
|
||||
|
||||
if (isLiteralExpression(value)) {
|
||||
this.props.onChange(value[1]);
|
||||
}
|
||||
else if (isLiteralExpression(lastValue)) {
|
||||
this.props.onChange(lastValue[1]);
|
||||
}
|
||||
else {
|
||||
this.props.onChange(fieldSpec.default);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const deleteStopBtn = (
|
||||
<Button
|
||||
onClick={this.onDelete}
|
||||
className="maputnik-delete-stop"
|
||||
>
|
||||
<MdDelete />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return <InputBlock
|
||||
error={this.props.error}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={deleteStopBtn}
|
||||
>
|
||||
<StringInput
|
||||
multi={true}
|
||||
value={stringifyPretty(this.props.value, {indent: 2})}
|
||||
spellCheck={false}
|
||||
onInput={this.onChange}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
|
@ -6,16 +6,47 @@ import Button from '../Button'
|
|||
import {MdFunctions, MdInsertChart} from 'react-icons/md'
|
||||
|
||||
|
||||
/**
|
||||
* So here we can't just check is `Array.isArray(value)` because certain
|
||||
* properties accept arrays as values, for example `text-font`. So we must try
|
||||
* and create an expression.
|
||||
*/
|
||||
function isExpression(value, fieldSpec={}) {
|
||||
if (!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
expression.createExpression(value, fieldSpec);
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default class FunctionButtons extends React.Component {
|
||||
static propTypes = {
|
||||
fieldSpec: PropTypes.object,
|
||||
onZoomClick: PropTypes.func,
|
||||
onDataClick: PropTypes.func,
|
||||
onExpressionClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
let makeZoomButton, makeDataButton
|
||||
let makeZoomButton, makeDataButton, expressionButton;
|
||||
|
||||
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
||||
expressionButton = (
|
||||
<Button
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onExpressionClick}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12.42,5.29C11.32,5.19 10.35,6 10.25,7.11L10,10H12.82V12H9.82L9.38,17.07C9.18,19.27 7.24,20.9 5.04,20.7C3.79,20.59 2.66,19.9 2,18.83L3.5,17.33C3.83,18.38 4.96,18.97 6,18.63C6.78,18.39 7.33,17.7 7.4,16.89L7.82,12H4.82V10H8L8.27,6.93C8.46,4.73 10.39,3.1 12.6,3.28C13.86,3.39 15,4.09 15.66,5.17L14.16,6.67C13.91,5.9 13.23,5.36 12.42,5.29M22,13.65L20.59,12.24L17.76,15.07L14.93,12.24L13.5,13.65L16.35,16.5L13.5,19.31L14.93,20.72L17.76,17.89L20.59,20.72L22,19.31L19.17,16.5L22,13.65Z" />
|
||||
</svg>
|
||||
</Button>
|
||||
);
|
||||
|
||||
makeZoomButton = <Button
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onZoomClick}
|
||||
|
@ -39,10 +70,14 @@ export default class FunctionButtons extends React.Component {
|
|||
/>
|
||||
</Button>
|
||||
}
|
||||
return <div>{makeDataButton}{makeZoomButton}</div>
|
||||
return <div>
|
||||
{expressionButton}
|
||||
{makeDataButton}
|
||||
{makeZoomButton}
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
return null
|
||||
return <div>{expressionButton}</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,12 @@ export default class SpecProperty extends React.Component {
|
|||
fieldSpec={this.props.fieldSpec}
|
||||
onZoomClick={this.props.onZoomClick}
|
||||
onDataClick={this.props.onDataClick}
|
||||
value={this.props.value}
|
||||
onExpressionClick={this.props.onExpressionClick}
|
||||
/>
|
||||
|
||||
return <InputBlock
|
||||
error={this.props.error}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={functionBtn}
|
||||
|
|
|
@ -124,6 +124,7 @@ export default class ZoomProperty extends React.Component {
|
|||
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||
|
||||
return <InputBlock
|
||||
error={this.props.error}
|
||||
key={key}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
|
|
|
@ -67,15 +67,25 @@ export default class CombiningFilterEditor extends React.Component {
|
|||
const filter = this.combiningFilter()
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
const {errors} = this.props;
|
||||
|
||||
const editorBlocks = filters.map((f, idx) => {
|
||||
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||
<SingleFilterEditor
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
</FilterEditorBlock>
|
||||
const error = errors[`filter[${idx+1}]`];
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||
<SingleFilterEditor
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
</FilterEditorBlock>
|
||||
{error &&
|
||||
<div className="maputnik-inline-error">{error.message}</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
})
|
||||
|
||||
//TODO: Implement support for nested filter
|
||||
|
|
|
@ -6,6 +6,7 @@ import FillIcon from './FillIcon.jsx'
|
|||
import SymbolIcon from './SymbolIcon.jsx'
|
||||
import BackgroundIcon from './BackgroundIcon.jsx'
|
||||
import CircleIcon from './CircleIcon.jsx'
|
||||
import MissingIcon from './MissingIcon.jsx'
|
||||
|
||||
class LayerIcon extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -25,6 +26,7 @@ class LayerIcon extends React.Component {
|
|||
case 'line': return <LineIcon {...iconProps} />
|
||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||
case 'circle': return <CircleIcon {...iconProps} />
|
||||
default: return <MissingIcon {...iconProps} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
11
src/components/icons/MissingIcon.jsx
Normal file
11
src/components/icons/MissingIcon.jsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
import {MdPriorityHigh} from 'react-icons/md'
|
||||
|
||||
|
||||
export default class MissingIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<MdPriorityHigh {...this.props} />
|
||||
)
|
||||
}
|
||||
}
|
|
@ -52,6 +52,11 @@ class InputBlock extends React.Component {
|
|||
<div className="maputnik-input-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
{this.props.error &&
|
||||
<div className="maputnik-inline-error">
|
||||
{this.props.error.message}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ class StringInput extends React.Component {
|
|||
onInput: PropTypes.func,
|
||||
multi: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -51,9 +52,14 @@ class StringInput extends React.Component {
|
|||
]
|
||||
}
|
||||
|
||||
if(!!this.props.disabled) {
|
||||
classes.push("maputnik-string--disabled");
|
||||
}
|
||||
|
||||
return React.createElement(tag, {
|
||||
"data-wd-key": this.props["data-wd-key"],
|
||||
spellCheck: !(tag === "input"),
|
||||
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
|
||||
disabled: this.props.disabled,
|
||||
className: classes.join(" "),
|
||||
style: this.props.style,
|
||||
value: this.state.value === undefined ? "" : this.state.value,
|
||||
|
|
|
@ -20,6 +20,10 @@ import { changeType, changeProperty } from '../../libs/layer'
|
|||
import layout from '../../config/layout.json'
|
||||
|
||||
|
||||
function getLayoutForType (type) {
|
||||
return layout[type] ? layout[type] : layout.invalid;
|
||||
}
|
||||
|
||||
function layoutGroups(layerType) {
|
||||
const layerGroup = {
|
||||
title: 'Layer',
|
||||
|
@ -33,7 +37,9 @@ function layoutGroups(layerType) {
|
|||
title: 'JSON Editor',
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
|
||||
return [layerGroup, filterGroup]
|
||||
.concat(getLayoutForType(layerType).groups)
|
||||
.concat([editorGroup])
|
||||
}
|
||||
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
|
@ -79,7 +85,7 @@ export default class LayerEditor extends React.Component {
|
|||
static getDerivedStateFromProps(props, state) {
|
||||
const additionalGroups = { ...state.editorGroups }
|
||||
|
||||
layout[props.layer.type].groups.forEach(group => {
|
||||
getLayoutForType(props.layer.type).groups.forEach(group => {
|
||||
if(!(group.title in additionalGroups)) {
|
||||
additionalGroups[group.title] = true
|
||||
}
|
||||
|
@ -118,6 +124,20 @@ export default class LayerEditor extends React.Component {
|
|||
if(this.props.layer.metadata) {
|
||||
comment = this.props.layer.metadata['maputnik:comment']
|
||||
}
|
||||
const {errors, layerIndex} = this.props;
|
||||
|
||||
const errorData = {};
|
||||
errors.forEach(error => {
|
||||
if (
|
||||
error.parsed &&
|
||||
error.parsed.type === "layer" &&
|
||||
error.parsed.data.index == layerIndex
|
||||
) {
|
||||
errorData[error.parsed.data.key] = {
|
||||
message: error.parsed.data.message
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
let sourceLayerIds;
|
||||
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
||||
|
@ -129,13 +149,16 @@ export default class LayerEditor extends React.Component {
|
|||
<LayerIdBlock
|
||||
value={this.props.layer.id}
|
||||
wdKey="layer-editor.layer-id"
|
||||
error={errorData.id}
|
||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
error={errorData.type}
|
||||
value={this.props.layer.type}
|
||||
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
||||
/>
|
||||
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
||||
error={errorData.sources}
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
value={this.props.layer.source}
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
|
@ -143,20 +166,24 @@ export default class LayerEditor extends React.Component {
|
|||
}
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
|
||||
<LayerSourceLayerBlock
|
||||
error={errorData['source-layer']}
|
||||
sourceLayerIds={sourceLayerIds}
|
||||
value={this.props.layer['source-layer']}
|
||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||
/>
|
||||
}
|
||||
<MinZoomBlock
|
||||
error={errorData.minzoom}
|
||||
value={this.props.layer.minzoom}
|
||||
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
||||
/>
|
||||
<MaxZoomBlock
|
||||
error={errorData.maxzoom}
|
||||
value={this.props.layer.maxzoom}
|
||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||
/>
|
||||
<CommentBlock
|
||||
error={errorData.comment}
|
||||
value={comment}
|
||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
||||
/>
|
||||
|
@ -164,6 +191,7 @@ export default class LayerEditor extends React.Component {
|
|||
case 'filter': return <div>
|
||||
<div className="maputnik-filter-editor-wrapper">
|
||||
<FilterEditor
|
||||
errors={errorData}
|
||||
filter={this.props.layer.filter}
|
||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||
onChange={f => this.changeProperty(null, 'filter', f)}
|
||||
|
@ -171,6 +199,7 @@ export default class LayerEditor extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
case 'properties': return <PropertyGroup
|
||||
errors={errorData}
|
||||
layer={this.props.layer}
|
||||
groupFields={fields}
|
||||
spec={this.props.spec}
|
||||
|
|
|
@ -181,10 +181,19 @@ class LayerListContainer extends React.Component {
|
|||
layers.forEach((layer, idxInGroup) => {
|
||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
||||
|
||||
const layerError = this.props.errors.find(error => {
|
||||
return (
|
||||
error.parsed &&
|
||||
error.parsed.type === "layer" &&
|
||||
error.parsed.data.index == idx
|
||||
);
|
||||
});
|
||||
|
||||
const listItem = <LayerListItem
|
||||
className={classnames({
|
||||
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
|
||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
|
||||
'maputnik-layer-list-item--error': !!layerError
|
||||
})}
|
||||
index={idx}
|
||||
key={layer.id}
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
|
|||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
class LayerTypeBlock extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -15,21 +16,11 @@ class LayerTypeBlock extends React.Component {
|
|||
render() {
|
||||
return <InputBlock label={"Type"} doc={latest.layer.type.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
['fill', 'Fill'],
|
||||
['line', 'Line'],
|
||||
['symbol', 'Symbol'],
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
['hillshade', 'Hillshade'],
|
||||
['heatmap', 'Heatmap'],
|
||||
]}
|
||||
onChange={this.props.onChange}
|
||||
<StringInput
|
||||
value={this.props.value}
|
||||
disabled={true}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ class MaxZoomBlock extends React.Component {
|
|||
|
||||
render() {
|
||||
return <InputBlock label={"Max Zoom"} doc={latest.layer.maxzoom.doc}
|
||||
error={this.props.error}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
|
|
|
@ -13,6 +13,7 @@ class MinZoomBlock extends React.Component {
|
|||
|
||||
render() {
|
||||
return <InputBlock label={"Min Zoom"} doc={latest.layer.minzoom.doc}
|
||||
error={this.props.error}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
|
|
|
@ -233,5 +233,8 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"invalid": {
|
||||
"groups": []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,13 +150,13 @@
|
|||
.maputnik-action-block {
|
||||
.maputnik-input-block-label {
|
||||
display: inline-block;
|
||||
width: 35%;
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
.maputnik-input-block-action {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
width: 15%;
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
.maputnik-input-block-action > div {
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
resize: vertical;
|
||||
height: 78px;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-number-container {
|
||||
|
|
|
@ -99,6 +99,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.maputnik-layer-list-item--error {
|
||||
color: $color-red;
|
||||
}
|
||||
|
||||
&-item-selected {
|
||||
color: $color-white;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
vertical-align: middle;
|
||||
padding: 0 $margin-2 0 0;
|
||||
|
||||
@extend .maputnik-icon-button;
|
||||
}
|
||||
|
@ -66,6 +67,7 @@
|
|||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
vertical-align: middle;
|
||||
padding: 0 $margin-2 0 0;
|
||||
|
||||
@extend .maputnik-icon-button;
|
||||
}
|
||||
|
|
|
@ -34,3 +34,12 @@
|
|||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.maputnik-inline-error {
|
||||
color: #a4a4a4;
|
||||
padding: 0.4em 0.4em;
|
||||
font-size: 0.9em;
|
||||
border: solid 1px $color-red;
|
||||
border-radius: 2px;
|
||||
margin: $margin-2 0px;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue