mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2024-12-28 17:41:16 +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: [],
|
errors: [],
|
||||||
infos: [],
|
infos: [],
|
||||||
mapStyle: style.emptyStyle,
|
mapStyle: style.emptyStyle,
|
||||||
|
hopefulMapStyle: style.emptyStyle,
|
||||||
selectedLayerIndex: 0,
|
selectedLayerIndex: 0,
|
||||||
sources: {},
|
sources: {},
|
||||||
vectorLayers: {},
|
vectorLayers: {},
|
||||||
|
@ -279,7 +280,7 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFonts(urlTemplate) {
|
updateFonts(urlTemplate) {
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
const metadata = this.state.hopefulMapStyle.metadata || {}
|
||||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||||
|
|
||||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
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) => {
|
onStyleChanged = (newStyle, save=true) => {
|
||||||
|
|
||||||
const errors = validate(newStyle, latest)
|
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(errors.length === 0) {
|
||||||
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
if(newStyle.glyphs !== this.state.hopefulMapStyle.glyphs) {
|
||||||
this.updateFonts(newStyle.glyphs)
|
this.updateFonts(newStyle.glyphs)
|
||||||
}
|
}
|
||||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
if(newStyle.sprite !== this.state.hopefulMapStyle.sprite) {
|
||||||
this.updateIcons(newStyle.sprite)
|
this.updateIcons(newStyle.sprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.revisionStore.addRevision(newStyle)
|
this.revisionStore.addRevision(newStyle)
|
||||||
if(save) this.saveStyle(newStyle)
|
if(save) this.saveStyle(newStyle)
|
||||||
this.setState({
|
this.setState({
|
||||||
|
hopefulMapStyle: newStyle,
|
||||||
mapStyle: newStyle,
|
mapStyle: newStyle,
|
||||||
errors: [],
|
errors: [],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
errors: errors.map(err => err.message)
|
hopefulMapStyle: newStyle,
|
||||||
|
errors: mappedErrors,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,7 +371,7 @@ export default class App extends React.Component {
|
||||||
|
|
||||||
onUndo = () => {
|
onUndo = () => {
|
||||||
const activeStyle = this.revisionStore.undo()
|
const activeStyle = this.revisionStore.undo()
|
||||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
const messages = undoMessages(this.state.hopefulMapStyle, activeStyle)
|
||||||
this.saveStyle(activeStyle)
|
this.saveStyle(activeStyle)
|
||||||
this.setState({
|
this.setState({
|
||||||
mapStyle: activeStyle,
|
mapStyle: activeStyle,
|
||||||
|
@ -354,7 +381,7 @@ export default class App extends React.Component {
|
||||||
|
|
||||||
onRedo = () => {
|
onRedo = () => {
|
||||||
const activeStyle = this.revisionStore.redo()
|
const activeStyle = this.revisionStore.redo()
|
||||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
const messages = redoMessages(this.state.hopefulMapStyle, activeStyle)
|
||||||
this.saveStyle(activeStyle)
|
this.saveStyle(activeStyle)
|
||||||
this.setState({
|
this.setState({
|
||||||
mapStyle: activeStyle,
|
mapStyle: activeStyle,
|
||||||
|
@ -364,7 +391,7 @@ export default class App extends React.Component {
|
||||||
|
|
||||||
onMoveLayer = (move) => {
|
onMoveLayer = (move) => {
|
||||||
let { oldIndex, newIndex } = move;
|
let { oldIndex, newIndex } = move;
|
||||||
let layers = this.state.mapStyle.layers;
|
let layers = this.state.hopefulMapStyle.layers;
|
||||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||||
newIndex = clamp(newIndex, 0, layers.length-1);
|
newIndex = clamp(newIndex, 0, layers.length-1);
|
||||||
if(oldIndex === newIndex) return;
|
if(oldIndex === newIndex) return;
|
||||||
|
@ -382,14 +409,14 @@ export default class App extends React.Component {
|
||||||
|
|
||||||
onLayersChange = (changedLayers) => {
|
onLayersChange = (changedLayers) => {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...this.state.mapStyle,
|
...this.state.hopefulMapStyle,
|
||||||
layers: changedLayers
|
layers: changedLayers
|
||||||
}
|
}
|
||||||
this.onStyleChanged(changedStyle)
|
this.onStyleChanged(changedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerDestroy = (layerId) => {
|
onLayerDestroy = (layerId) => {
|
||||||
let layers = this.state.mapStyle.layers;
|
let layers = this.state.hopefulMapStyle.layers;
|
||||||
const remainingLayers = layers.slice(0);
|
const remainingLayers = layers.slice(0);
|
||||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||||
remainingLayers.splice(idx, 1);
|
remainingLayers.splice(idx, 1);
|
||||||
|
@ -408,7 +435,7 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerVisibilityToggle = (layerId) => {
|
onLayerVisibilityToggle = (layerId) => {
|
||||||
let layers = this.state.mapStyle.layers;
|
let layers = this.state.hopefulMapStyle.layers;
|
||||||
const changedLayers = layers.slice(0)
|
const changedLayers = layers.slice(0)
|
||||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||||
|
|
||||||
|
@ -423,7 +450,7 @@ export default class App extends React.Component {
|
||||||
|
|
||||||
|
|
||||||
onLayerIdChange = (oldId, newId) => {
|
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)
|
const idx = style.indexOfLayer(changedLayers, oldId)
|
||||||
|
|
||||||
changedLayers[idx] = {
|
changedLayers[idx] = {
|
||||||
|
@ -435,7 +462,8 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerChanged = (layer) => {
|
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)
|
const idx = style.indexOfLayer(changedLayers, layer.id)
|
||||||
changedLayers[idx] = layer
|
changedLayers[idx] = layer
|
||||||
|
|
||||||
|
@ -472,7 +500,7 @@ export default class App extends React.Component {
|
||||||
fetchSources() {
|
fetchSources() {
|
||||||
const sourceList = {...this.state.sources};
|
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)) {
|
if(sourceList.hasOwnProperty(key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -596,7 +624,7 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerSelect = (layerId) => {
|
onLayerSelect = (layerId) => {
|
||||||
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
const idx = style.indexOfLayer(this.state.hopefulMapStyle.layers, layerId)
|
||||||
this.setState({ selectedLayerIndex: idx })
|
this.setState({ selectedLayerIndex: idx })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -636,14 +664,14 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const layers = this.state.mapStyle.layers || []
|
const layers = this.state.hopefulMapStyle.layers || []
|
||||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
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
|
const toolbar = <Toolbar
|
||||||
renderer={this._getRenderer()}
|
renderer={this._getRenderer()}
|
||||||
mapState={this.state.mapState}
|
mapState={this.state.mapState}
|
||||||
mapStyle={this.state.mapStyle}
|
mapStyle={this.state.hopefulMapStyle}
|
||||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||||
sources={this.state.sources}
|
sources={this.state.sources}
|
||||||
onStyleChanged={this.onStyleChanged}
|
onStyleChanged={this.onStyleChanged}
|
||||||
|
@ -662,6 +690,7 @@ export default class App extends React.Component {
|
||||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||||
layers={layers}
|
layers={layers}
|
||||||
sources={this.state.sources}
|
sources={this.state.sources}
|
||||||
|
errors={this.state.errors}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
const layerEditor = selectedLayer ? <LayerEditor
|
const layerEditor = selectedLayer ? <LayerEditor
|
||||||
|
@ -669,7 +698,7 @@ export default class App extends React.Component {
|
||||||
layer={selectedLayer}
|
layer={selectedLayer}
|
||||||
layerIndex={this.state.selectedLayerIndex}
|
layerIndex={this.state.selectedLayerIndex}
|
||||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
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}
|
sources={this.state.sources}
|
||||||
vectorLayers={this.state.vectorLayers}
|
vectorLayers={this.state.vectorLayers}
|
||||||
spec={this.state.spec}
|
spec={this.state.spec}
|
||||||
|
@ -679,9 +708,11 @@ export default class App extends React.Component {
|
||||||
onLayerCopy={this.onLayerCopy}
|
onLayerCopy={this.onLayerCopy}
|
||||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||||
onLayerIdChange={this.onLayerIdChange}
|
onLayerIdChange={this.onLayerIdChange}
|
||||||
|
errors={this.state.errors}
|
||||||
/> : null
|
/> : null
|
||||||
|
|
||||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||||
|
mapStyle={this.state.mapStyle}
|
||||||
errors={this.state.errors}
|
errors={this.state.errors}
|
||||||
infos={this.state.infos}
|
infos={this.state.infos}
|
||||||
/> : null
|
/> : null
|
||||||
|
@ -704,7 +735,7 @@ export default class App extends React.Component {
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||||
/>
|
/>
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
mapStyle={this.state.mapStyle}
|
mapStyle={this.state.hopefulMapStyle}
|
||||||
onStyleChanged={this.onStyleChanged}
|
onStyleChanged={this.onStyleChanged}
|
||||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||||
isOpen={this.state.isOpen.settings}
|
isOpen={this.state.isOpen.settings}
|
||||||
|
@ -712,7 +743,7 @@ export default class App extends React.Component {
|
||||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||||
/>
|
/>
|
||||||
<ExportModal
|
<ExportModal
|
||||||
mapStyle={this.state.mapStyle}
|
mapStyle={this.state.hopefulMapStyle}
|
||||||
onStyleChanged={this.onStyleChanged}
|
onStyleChanged={this.onStyleChanged}
|
||||||
isOpen={this.state.isOpen.export}
|
isOpen={this.state.isOpen.export}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||||
|
@ -723,7 +754,7 @@ export default class App extends React.Component {
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||||
/>
|
/>
|
||||||
<SourcesModal
|
<SourcesModal
|
||||||
mapStyle={this.state.mapStyle}
|
mapStyle={this.state.hopefulMapStyle}
|
||||||
onStyleChanged={this.onStyleChanged}
|
onStyleChanged={this.onStyleChanged}
|
||||||
isOpen={this.state.isOpen.sources}
|
isOpen={this.state.isOpen.sources}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||||
|
|
|
@ -8,8 +8,23 @@ class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const errors = this.props.errors.map((m, i) => {
|
const errors = this.props.errors.map((error, idx) => {
|
||||||
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
|
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) => {
|
const infos = this.props.infos.map((m, i) => {
|
||||||
|
|
|
@ -4,6 +4,9 @@ import PropTypes from 'prop-types'
|
||||||
import SpecProperty from './_SpecProperty'
|
import SpecProperty from './_SpecProperty'
|
||||||
import DataProperty from './_DataProperty'
|
import DataProperty from './_DataProperty'
|
||||||
import ZoomProperty from './_ZoomProperty'
|
import ZoomProperty from './_ZoomProperty'
|
||||||
|
import ExpressionProperty from './_ExpressionProperty'
|
||||||
|
import {expression} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function isZoomField(value) {
|
function isZoomField(value) {
|
||||||
|
@ -14,6 +17,24 @@ function isDataField(value) {
|
||||||
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
|
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
|
* 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)
|
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 = () => {
|
makeDataFunction = () => {
|
||||||
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
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"
|
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||||
let specField;
|
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 = (
|
specField = (
|
||||||
<ZoomProperty
|
<ZoomProperty
|
||||||
|
error={this.props.error}
|
||||||
onChange={this.props.onChange.bind(this)}
|
onChange={this.props.onChange.bind(this)}
|
||||||
fieldName={this.props.fieldName}
|
fieldName={this.props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
@ -141,6 +179,7 @@ export default class FunctionSpecProperty extends React.Component {
|
||||||
else if (isDataField(this.props.value)) {
|
else if (isDataField(this.props.value)) {
|
||||||
specField = (
|
specField = (
|
||||||
<DataProperty
|
<DataProperty
|
||||||
|
error={this.props.error}
|
||||||
onChange={this.props.onChange.bind(this)}
|
onChange={this.props.onChange.bind(this)}
|
||||||
fieldName={this.props.fieldName}
|
fieldName={this.props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
@ -153,12 +192,14 @@ export default class FunctionSpecProperty extends React.Component {
|
||||||
else {
|
else {
|
||||||
specField = (
|
specField = (
|
||||||
<SpecProperty
|
<SpecProperty
|
||||||
|
error={this.props.error}
|
||||||
onChange={this.props.onChange.bind(this)}
|
onChange={this.props.onChange.bind(this)}
|
||||||
fieldName={this.props.fieldName}
|
fieldName={this.props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onZoomClick={this.makeZoomFunction}
|
onZoomClick={this.makeZoomFunction}
|
||||||
onDataClick={this.makeDataFunction}
|
onDataClick={this.makeDataFunction}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,14 +48,18 @@ export default class PropertyGroup extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {errors} = this.props;
|
||||||
const fields = this.props.groupFields.map(fieldName => {
|
const fields = this.props.groupFields.map(fieldName => {
|
||||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
||||||
|
|
||||||
const paint = this.props.layer.paint || {}
|
const paint = this.props.layer.paint || {}
|
||||||
const layout = this.props.layer.layout || {}
|
const layout = this.props.layer.layout || {}
|
||||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||||
|
const fieldType = fieldName in paint ? 'paint' : 'layout';
|
||||||
|
const errorKey = fieldType+"."+fieldName;
|
||||||
|
|
||||||
return <FunctionSpecField
|
return <FunctionSpecField
|
||||||
|
error={errors[errorKey]}
|
||||||
onChange={this.onPropertyChange}
|
onChange={this.onPropertyChange}
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
|
|
|
@ -200,6 +200,7 @@ export default class DataProperty extends React.Component {
|
||||||
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">
|
||||||
<InputBlock
|
<InputBlock
|
||||||
|
error={this.props.error}
|
||||||
doc={this.props.fieldSpec.doc}
|
doc={this.props.fieldSpec.doc}
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
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'
|
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 {
|
export default class FunctionButtons extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
fieldSpec: PropTypes.object,
|
fieldSpec: PropTypes.object,
|
||||||
onZoomClick: PropTypes.func,
|
onZoomClick: PropTypes.func,
|
||||||
onDataClick: PropTypes.func,
|
onDataClick: PropTypes.func,
|
||||||
|
onExpressionClick: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let makeZoomButton, makeDataButton
|
let makeZoomButton, makeDataButton, expressionButton;
|
||||||
|
|
||||||
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
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
|
makeZoomButton = <Button
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
onClick={this.props.onZoomClick}
|
onClick={this.props.onZoomClick}
|
||||||
|
@ -39,10 +70,14 @@ export default class FunctionButtons extends React.Component {
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
return <div>{makeDataButton}{makeZoomButton}</div>
|
return <div>
|
||||||
|
{expressionButton}
|
||||||
|
{makeDataButton}
|
||||||
|
{makeZoomButton}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null
|
return <div>{expressionButton}</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,12 @@ export default class SpecProperty extends React.Component {
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
onZoomClick={this.props.onZoomClick}
|
onZoomClick={this.props.onZoomClick}
|
||||||
onDataClick={this.props.onDataClick}
|
onDataClick={this.props.onDataClick}
|
||||||
|
value={this.props.value}
|
||||||
|
onExpressionClick={this.props.onExpressionClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
return <InputBlock
|
return <InputBlock
|
||||||
|
error={this.props.error}
|
||||||
doc={this.props.fieldSpec.doc}
|
doc={this.props.fieldSpec.doc}
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
action={functionBtn}
|
action={functionBtn}
|
||||||
|
|
|
@ -124,6 +124,7 @@ export default class ZoomProperty extends React.Component {
|
||||||
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||||
|
|
||||||
return <InputBlock
|
return <InputBlock
|
||||||
|
error={this.props.error}
|
||||||
key={key}
|
key={key}
|
||||||
doc={this.props.fieldSpec.doc}
|
doc={this.props.fieldSpec.doc}
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
|
|
|
@ -67,15 +67,25 @@ export default class CombiningFilterEditor extends React.Component {
|
||||||
const filter = this.combiningFilter()
|
const filter = this.combiningFilter()
|
||||||
let combiningOp = filter[0]
|
let combiningOp = filter[0]
|
||||||
let filters = filter.slice(1)
|
let filters = filter.slice(1)
|
||||||
|
const {errors} = this.props;
|
||||||
|
|
||||||
const editorBlocks = filters.map((f, idx) => {
|
const editorBlocks = filters.map((f, idx) => {
|
||||||
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
const error = errors[`filter[${idx+1}]`];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||||
<SingleFilterEditor
|
<SingleFilterEditor
|
||||||
properties={this.props.properties}
|
properties={this.props.properties}
|
||||||
filter={f}
|
filter={f}
|
||||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||||
/>
|
/>
|
||||||
</FilterEditorBlock>
|
</FilterEditorBlock>
|
||||||
|
{error &&
|
||||||
|
<div className="maputnik-inline-error">{error.message}</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
//TODO: Implement support for nested filter
|
//TODO: Implement support for nested filter
|
||||||
|
|
|
@ -6,6 +6,7 @@ import FillIcon from './FillIcon.jsx'
|
||||||
import SymbolIcon from './SymbolIcon.jsx'
|
import SymbolIcon from './SymbolIcon.jsx'
|
||||||
import BackgroundIcon from './BackgroundIcon.jsx'
|
import BackgroundIcon from './BackgroundIcon.jsx'
|
||||||
import CircleIcon from './CircleIcon.jsx'
|
import CircleIcon from './CircleIcon.jsx'
|
||||||
|
import MissingIcon from './MissingIcon.jsx'
|
||||||
|
|
||||||
class LayerIcon extends React.Component {
|
class LayerIcon extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -25,6 +26,7 @@ class LayerIcon extends React.Component {
|
||||||
case 'line': return <LineIcon {...iconProps} />
|
case 'line': return <LineIcon {...iconProps} />
|
||||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||||
case 'circle': return <CircleIcon {...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">
|
<div className="maputnik-input-block-content">
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
{this.props.error &&
|
||||||
|
<div className="maputnik-inline-error">
|
||||||
|
{this.props.error.message}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ class StringInput extends React.Component {
|
||||||
onInput: PropTypes.func,
|
onInput: PropTypes.func,
|
||||||
multi: PropTypes.bool,
|
multi: PropTypes.bool,
|
||||||
required: PropTypes.bool,
|
required: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -51,9 +52,14 @@ class StringInput extends React.Component {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!!this.props.disabled) {
|
||||||
|
classes.push("maputnik-string--disabled");
|
||||||
|
}
|
||||||
|
|
||||||
return React.createElement(tag, {
|
return React.createElement(tag, {
|
||||||
"data-wd-key": this.props["data-wd-key"],
|
"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(" "),
|
className: classes.join(" "),
|
||||||
style: this.props.style,
|
style: this.props.style,
|
||||||
value: this.state.value === undefined ? "" : this.state.value,
|
value: this.state.value === undefined ? "" : this.state.value,
|
||||||
|
|
|
@ -20,6 +20,10 @@ import { changeType, changeProperty } from '../../libs/layer'
|
||||||
import layout from '../../config/layout.json'
|
import layout from '../../config/layout.json'
|
||||||
|
|
||||||
|
|
||||||
|
function getLayoutForType (type) {
|
||||||
|
return layout[type] ? layout[type] : layout.invalid;
|
||||||
|
}
|
||||||
|
|
||||||
function layoutGroups(layerType) {
|
function layoutGroups(layerType) {
|
||||||
const layerGroup = {
|
const layerGroup = {
|
||||||
title: 'Layer',
|
title: 'Layer',
|
||||||
|
@ -33,7 +37,9 @@ function layoutGroups(layerType) {
|
||||||
title: 'JSON Editor',
|
title: 'JSON Editor',
|
||||||
type: 'jsoneditor'
|
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. */
|
/** Layer editor supporting multiple types of layers. */
|
||||||
|
@ -79,7 +85,7 @@ export default class LayerEditor extends React.Component {
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props, state) {
|
||||||
const additionalGroups = { ...state.editorGroups }
|
const additionalGroups = { ...state.editorGroups }
|
||||||
|
|
||||||
layout[props.layer.type].groups.forEach(group => {
|
getLayoutForType(props.layer.type).groups.forEach(group => {
|
||||||
if(!(group.title in additionalGroups)) {
|
if(!(group.title in additionalGroups)) {
|
||||||
additionalGroups[group.title] = true
|
additionalGroups[group.title] = true
|
||||||
}
|
}
|
||||||
|
@ -118,6 +124,20 @@ export default class LayerEditor extends React.Component {
|
||||||
if(this.props.layer.metadata) {
|
if(this.props.layer.metadata) {
|
||||||
comment = this.props.layer.metadata['maputnik:comment']
|
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;
|
let sourceLayerIds;
|
||||||
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
||||||
|
@ -129,13 +149,16 @@ export default class LayerEditor extends React.Component {
|
||||||
<LayerIdBlock
|
<LayerIdBlock
|
||||||
value={this.props.layer.id}
|
value={this.props.layer.id}
|
||||||
wdKey="layer-editor.layer-id"
|
wdKey="layer-editor.layer-id"
|
||||||
|
error={errorData.id}
|
||||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||||
/>
|
/>
|
||||||
<LayerTypeBlock
|
<LayerTypeBlock
|
||||||
|
error={errorData.type}
|
||||||
value={this.props.layer.type}
|
value={this.props.layer.type}
|
||||||
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
||||||
/>
|
/>
|
||||||
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
||||||
|
error={errorData.sources}
|
||||||
sourceIds={Object.keys(this.props.sources)}
|
sourceIds={Object.keys(this.props.sources)}
|
||||||
value={this.props.layer.source}
|
value={this.props.layer.source}
|
||||||
onChange={v => this.changeProperty(null, 'source', v)}
|
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 &&
|
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
|
||||||
<LayerSourceLayerBlock
|
<LayerSourceLayerBlock
|
||||||
|
error={errorData['source-layer']}
|
||||||
sourceLayerIds={sourceLayerIds}
|
sourceLayerIds={sourceLayerIds}
|
||||||
value={this.props.layer['source-layer']}
|
value={this.props.layer['source-layer']}
|
||||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<MinZoomBlock
|
<MinZoomBlock
|
||||||
|
error={errorData.minzoom}
|
||||||
value={this.props.layer.minzoom}
|
value={this.props.layer.minzoom}
|
||||||
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
||||||
/>
|
/>
|
||||||
<MaxZoomBlock
|
<MaxZoomBlock
|
||||||
|
error={errorData.maxzoom}
|
||||||
value={this.props.layer.maxzoom}
|
value={this.props.layer.maxzoom}
|
||||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||||
/>
|
/>
|
||||||
<CommentBlock
|
<CommentBlock
|
||||||
|
error={errorData.comment}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
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>
|
case 'filter': return <div>
|
||||||
<div className="maputnik-filter-editor-wrapper">
|
<div className="maputnik-filter-editor-wrapper">
|
||||||
<FilterEditor
|
<FilterEditor
|
||||||
|
errors={errorData}
|
||||||
filter={this.props.layer.filter}
|
filter={this.props.layer.filter}
|
||||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||||
onChange={f => this.changeProperty(null, 'filter', f)}
|
onChange={f => this.changeProperty(null, 'filter', f)}
|
||||||
|
@ -171,6 +199,7 @@ export default class LayerEditor extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
case 'properties': return <PropertyGroup
|
case 'properties': return <PropertyGroup
|
||||||
|
errors={errorData}
|
||||||
layer={this.props.layer}
|
layer={this.props.layer}
|
||||||
groupFields={fields}
|
groupFields={fields}
|
||||||
spec={this.props.spec}
|
spec={this.props.spec}
|
||||||
|
|
|
@ -181,10 +181,19 @@ class LayerListContainer extends React.Component {
|
||||||
layers.forEach((layer, idxInGroup) => {
|
layers.forEach((layer, idxInGroup) => {
|
||||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
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
|
const listItem = <LayerListItem
|
||||||
className={classnames({
|
className={classnames({
|
||||||
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
'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}
|
index={idx}
|
||||||
key={layer.id}
|
key={layer.id}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import StringInput from '../inputs/StringInput'
|
||||||
|
|
||||||
class LayerTypeBlock extends React.Component {
|
class LayerTypeBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -15,21 +16,11 @@ class LayerTypeBlock extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Type"} doc={latest.layer.type.doc}
|
return <InputBlock label={"Type"} doc={latest.layer.type.doc}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<SelectInput
|
<StringInput
|
||||||
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}
|
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ class MaxZoomBlock extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Max Zoom"} doc={latest.layer.maxzoom.doc}
|
return <InputBlock label={"Max Zoom"} doc={latest.layer.maxzoom.doc}
|
||||||
|
error={this.props.error}
|
||||||
data-wd-key="max-zoom"
|
data-wd-key="max-zoom"
|
||||||
>
|
>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
|
|
@ -13,6 +13,7 @@ class MinZoomBlock extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Min Zoom"} doc={latest.layer.minzoom.doc}
|
return <InputBlock label={"Min Zoom"} doc={latest.layer.minzoom.doc}
|
||||||
|
error={this.props.error}
|
||||||
data-wd-key="min-zoom"
|
data-wd-key="min-zoom"
|
||||||
>
|
>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
|
|
@ -233,5 +233,8 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"invalid": {
|
||||||
|
"groups": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,13 +150,13 @@
|
||||||
.maputnik-action-block {
|
.maputnik-action-block {
|
||||||
.maputnik-input-block-label {
|
.maputnik-input-block-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 35%;
|
width: 32%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-input-block-action {
|
.maputnik-input-block-action {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 15%;
|
width: 18%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-input-block-action > div {
|
.maputnik-input-block-action > div {
|
||||||
|
|
|
@ -25,6 +25,11 @@
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
height: 78px;
|
height: 78px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-number-container {
|
.maputnik-number-container {
|
||||||
|
|
|
@ -99,6 +99,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.maputnik-layer-list-item--error {
|
||||||
|
color: $color-red;
|
||||||
|
}
|
||||||
|
|
||||||
&-item-selected {
|
&-item-selected {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
padding: 0 $margin-2 0 0;
|
||||||
|
|
||||||
@extend .maputnik-icon-button;
|
@extend .maputnik-icon-button;
|
||||||
}
|
}
|
||||||
|
@ -66,6 +67,7 @@
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
padding: 0 $margin-2 0 0;
|
||||||
|
|
||||||
@extend .maputnik-icon-button;
|
@extend .maputnik-icon-button;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,3 +34,12 @@
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 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