mirror of
https://github.com/maputnik/editor.git
synced 2026-01-09 15:00:01 +00:00
Compare commits
25 Commits
v1.7.0-bet
...
v1.7.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
184bfeeaf8 | ||
|
|
e45f8d960d | ||
|
|
1fede3af3a | ||
|
|
5ad74048bd | ||
|
|
a0a91474de | ||
|
|
c3670701e5 | ||
|
|
86923330d9 | ||
|
|
4517148e5a | ||
|
|
0433d66f45 | ||
|
|
0c592bacab | ||
|
|
d98637cb12 | ||
|
|
1070209cb5 | ||
|
|
b6189f77c4 | ||
|
|
25322a3952 | ||
|
|
5943c6f282 | ||
|
|
090a26bb40 | ||
|
|
af03b010a4 | ||
|
|
578a920b6d | ||
|
|
0858a16ffc | ||
|
|
7cfe0563bc | ||
|
|
ee72389534 | ||
|
|
8f722c59de | ||
|
|
94d2e958eb | ||
|
|
d931c7cb38 | ||
|
|
6b45dc8b4d |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "1.7.0-beta2",
|
||||
"version": "1.7.0-beta4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "1.7.0-beta2",
|
||||
"version": "1.7.0-beta4",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
|
||||
@@ -345,6 +345,7 @@ export default class App extends React.Component {
|
||||
}
|
||||
|
||||
const mappedErrors = layerErrors.concat(errors).map(error => {
|
||||
// Special case: Duplicate layer id
|
||||
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
|
||||
if (dupMatch) {
|
||||
const [matchStr, index, message] = dupMatch;
|
||||
@@ -361,7 +362,23 @@ export default class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// duplicate layer id
|
||||
// Special case: Invalid source
|
||||
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
|
||||
if (invalidSourceMatch) {
|
||||
const [matchStr, index, message] = invalidSourceMatch;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
type: "layer",
|
||||
data: {
|
||||
index: parseInt(index, 10),
|
||||
key: "source",
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
||||
if (layerMatch) {
|
||||
const [matchStr, index, group, property, message] = layerMatch;
|
||||
|
||||
@@ -6,12 +6,21 @@ import DataProperty from './_DataProperty'
|
||||
import ZoomProperty from './_ZoomProperty'
|
||||
import ExpressionProperty from './_ExpressionProperty'
|
||||
import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec';
|
||||
import {findDefaultFromSpec} from '../util/spec-helper';
|
||||
|
||||
|
||||
function isLiteralExpression (value) {
|
||||
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) {
|
||||
return (
|
||||
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 (
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
@@ -45,6 +62,13 @@ function isDataField(value) {
|
||||
);
|
||||
}
|
||||
|
||||
function isDataField(value) {
|
||||
return (
|
||||
isIdentityProperty(value) ||
|
||||
isDataStopProperty(value)
|
||||
);
|
||||
}
|
||||
|
||||
function isPrimative (value) {
|
||||
const valid = ["string", "boolean", "number"];
|
||||
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
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
@@ -206,7 +212,16 @@ export default class FunctionSpecProperty extends React.Component {
|
||||
undoExpression = () => {
|
||||
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.setState({
|
||||
dataType: "value",
|
||||
@@ -217,6 +232,7 @@ export default class FunctionSpecProperty extends React.Component {
|
||||
canUndo = () => {
|
||||
const {value, fieldSpec} = this.props;
|
||||
return (
|
||||
isGetExpression(value) ||
|
||||
isLiteralExpression(value) ||
|
||||
isPrimative(value) ||
|
||||
(Array.isArray(value) && fieldSpec.type === "array")
|
||||
@@ -230,6 +246,9 @@ export default class FunctionSpecProperty extends React.Component {
|
||||
if (typeof(value) === "object" && 'stops' in value) {
|
||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||
}
|
||||
else if (isIdentityProperty(value)) {
|
||||
expression = ["get", value.property];
|
||||
}
|
||||
else {
|
||||
expression = ["literal", value || this.props.fieldSpec.default];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
@@ -10,6 +11,7 @@ import DocLabel from './DocLabel'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import docUid from '../../libs/document-uid'
|
||||
import sortNumerically from '../../libs/sort-numerically'
|
||||
import {findDefaultFromSpec} from '../util/spec-helper';
|
||||
|
||||
import labelFromFieldName from './_labelFromFieldName'
|
||||
import DeleteStopButton from './_DeleteStopButton'
|
||||
@@ -89,10 +91,10 @@ export default class DataProperty extends React.Component {
|
||||
|
||||
getDataFunctionTypes(fieldSpec) {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return ["categorical", "interval", "exponential"]
|
||||
return ["categorical", "interval", "exponential", "identity"]
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
onChange = (fieldName, value) => {
|
||||
if (value.type === "identity") {
|
||||
value = {
|
||||
type: value.type,
|
||||
property: value.property,
|
||||
};
|
||||
}
|
||||
else {
|
||||
const stopValue = value.type === 'categorical' ? '' : 0;
|
||||
value = {
|
||||
property: "",
|
||||
type: value.type,
|
||||
// Default props if they don't already exist.
|
||||
stops: [
|
||||
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
||||
],
|
||||
...value,
|
||||
}
|
||||
}
|
||||
this.props.onChange(fieldName, value);
|
||||
}
|
||||
|
||||
changeStop(changeIdx, stopData, value) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
||||
@@ -133,7 +158,7 @@ export default class DataProperty extends React.Component {
|
||||
...this.props.value,
|
||||
stops: orderedStops,
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
this.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
changeDataProperty(propName, propVal) {
|
||||
@@ -143,7 +168,7 @@ export default class DataProperty extends React.Component {
|
||||
else {
|
||||
delete this.props.value[propName]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, this.props.value)
|
||||
this.onChange(this.props.fieldName, this.props.value)
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -153,69 +178,72 @@ export default class DataProperty extends React.Component {
|
||||
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
|
||||
}
|
||||
|
||||
const dataFields = this.props.value.stops.map((stop, idx) => {
|
||||
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
|
||||
const key = this.state.refs[idx];
|
||||
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
|
||||
const value = stop[1]
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||
let dataFields;
|
||||
if (this.props.value.stops) {
|
||||
dataFields = this.props.value.stops.map((stop, idx) => {
|
||||
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
|
||||
const key = this.state.refs[idx];
|
||||
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
|
||||
const value = stop[1]
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||
|
||||
const dataProps = {
|
||||
label: "Data value",
|
||||
value: dataLevel,
|
||||
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
|
||||
}
|
||||
const dataProps = {
|
||||
label: "Data value",
|
||||
value: dataLevel,
|
||||
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
|
||||
}
|
||||
|
||||
let dataInput;
|
||||
if(this.props.value.type === "categorical") {
|
||||
dataInput = <StringInput {...dataProps} />
|
||||
}
|
||||
else {
|
||||
dataInput = <NumberInput {...dataProps} />
|
||||
}
|
||||
let dataInput;
|
||||
if(this.props.value.type === "categorical") {
|
||||
dataInput = <StringInput {...dataProps} />
|
||||
}
|
||||
else {
|
||||
dataInput = <NumberInput {...dataProps} />
|
||||
}
|
||||
|
||||
let zoomInput = null;
|
||||
if(zoomLevel !== undefined) {
|
||||
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
|
||||
<NumberInput
|
||||
value={zoomLevel}
|
||||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
let zoomInput = null;
|
||||
if(zoomLevel !== undefined) {
|
||||
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
|
||||
<NumberInput
|
||||
value={zoomLevel}
|
||||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
|
||||
const foundErrors = Object.entries(errors).filter(([key, error]) => {
|
||||
return key.startsWith(errorKeyStart);
|
||||
});
|
||||
const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
|
||||
const foundErrors = Object.entries(errors).filter(([key, error]) => {
|
||||
return key.startsWith(errorKeyStart);
|
||||
});
|
||||
|
||||
const message = foundErrors.map(([key, error]) => {
|
||||
return error.message;
|
||||
}).join("");
|
||||
const error = message ? {message} : undefined;
|
||||
const message = foundErrors.map(([key, error]) => {
|
||||
return error.message;
|
||||
}).join("");
|
||||
const error = message ? {message} : undefined;
|
||||
|
||||
return <InputBlock
|
||||
error={error}
|
||||
key={key}
|
||||
action={deleteStopBtn}
|
||||
label=""
|
||||
>
|
||||
{zoomInput}
|
||||
<div className="maputnik-data-spec-property-stop-data">
|
||||
{dataInput}
|
||||
</div>
|
||||
<div className="maputnik-data-spec-property-stop-value">
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
|
||||
/>
|
||||
</div>
|
||||
</InputBlock>
|
||||
})
|
||||
return <InputBlock
|
||||
error={error}
|
||||
key={key}
|
||||
action={deleteStopBtn}
|
||||
label=""
|
||||
>
|
||||
{zoomInput}
|
||||
<div className="maputnik-data-spec-property-stop-data">
|
||||
{dataInput}
|
||||
</div>
|
||||
<div className="maputnik-data-spec-property-stop-value">
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
|
||||
/>
|
||||
</div>
|
||||
</InputBlock>
|
||||
})
|
||||
}
|
||||
|
||||
return <div className="maputnik-data-spec-block">
|
||||
<div className="maputnik-data-spec-property">
|
||||
@@ -223,18 +251,6 @@ export default class DataProperty extends React.Component {
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
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">
|
||||
<DocLabel
|
||||
label="Type"
|
||||
@@ -250,31 +266,53 @@ export default class DataProperty extends React.Component {
|
||||
</div>
|
||||
<div className="maputnik-data-spec-property-group">
|
||||
<DocLabel
|
||||
label="Default"
|
||||
label="Property"
|
||||
/>
|
||||
<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)}
|
||||
<StringInput
|
||||
value={this.props.value.property}
|
||||
title={"Input a data property to base styles off of."}
|
||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
{dataFields}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.props.onAddStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
{dataFields &&
|
||||
<>
|
||||
{dataFields}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.props.onAddStop.bind(this)}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add stop
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
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>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
@@ -176,13 +177,17 @@ export default class ZoomProperty extends React.Component {
|
||||
className="maputnik-add-stop"
|
||||
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
|
||||
className="maputnik-add-stop"
|
||||
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>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||
|
||||
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
@@ -261,8 +262,11 @@ export default class CombiningFilterEditor extends React.Component {
|
||||
<Button
|
||||
data-wd-key="layer-filter-button"
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem}>
|
||||
Add filter
|
||||
onClick={this.addFilterItem}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add filter
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -4,7 +4,7 @@ import AutocompleteInput from './AutocompleteInput'
|
||||
|
||||
class FontInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array.isRequired,
|
||||
value: PropTypes.array,
|
||||
default: PropTypes.array,
|
||||
fonts: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
@@ -16,7 +16,7 @@ class FontInput extends React.Component {
|
||||
}
|
||||
|
||||
get values() {
|
||||
const out = this.props.value || this.props.default.slice(1) || [""];
|
||||
const out = this.props.value || this.props.default || [];
|
||||
|
||||
// Always put a "" in the last field to you can keep adding entries
|
||||
if (out[out.length-1] !== ""){
|
||||
|
||||
@@ -168,7 +168,7 @@ export default class LayerEditor extends React.Component {
|
||||
)}
|
||||
/>
|
||||
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
||||
error={errorData.sources}
|
||||
error={errorData.source}
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
value={this.props.layer.source}
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
|
||||
@@ -11,6 +11,7 @@ class LayerSourceBlock extends React.Component {
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceIds: PropTypes.array,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -19,7 +20,10 @@ class LayerSourceBlock extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source"} fieldSpec={latest.layer.source}
|
||||
return <InputBlock
|
||||
label={"Source"}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={this.props.error}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<AutocompleteInput
|
||||
|
||||
@@ -21,12 +21,19 @@ function renderProperties(feature) {
|
||||
})
|
||||
}
|
||||
|
||||
function renderFeatureId(feature) {
|
||||
return <InputBlock key={"feature-id"} label={"feature_id"}>
|
||||
<StringInput value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
function renderFeature(feature, idx) {
|
||||
return <div key={`${feature.sourceLayer}-${idx}`}>
|
||||
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
||||
<InputBlock key={"property-type"} label={"$type"}>
|
||||
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
||||
</InputBlock>
|
||||
{renderFeatureId(feature)}
|
||||
{renderProperties(feature)}
|
||||
</div>
|
||||
}
|
||||
@@ -36,7 +43,7 @@ function removeDuplicatedFeatures(features) {
|
||||
|
||||
features.forEach(feature => {
|
||||
const featureIndex = uniqueFeatures.findIndex(feature2 => {
|
||||
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
||||
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
||||
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
|
||||
})
|
||||
|
||||
|
||||
@@ -104,17 +104,24 @@ export default class MapboxGlMap extends React.Component {
|
||||
|
||||
this.updateMapFromProps(this.props);
|
||||
|
||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
||||
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
|
||||
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
|
||||
// eslint-disable-next-line
|
||||
this.state.inspect._popupBlocked = false;
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
if(this.props.inspectModeEnabled) {
|
||||
this.state.inspect.render()
|
||||
}
|
||||
|
||||
if (map) {
|
||||
if (this.props.inspectModeEnabled) {
|
||||
// HACK: We need to work out why we need to do this and what's causing
|
||||
// this error. I'm assuming an issue with mapbox-gl update and
|
||||
// mapbox-gl-inspect.
|
||||
try {
|
||||
this.state.inspect.render();
|
||||
} catch(err) {
|
||||
console.error("FIXME: Caught error", err);
|
||||
}
|
||||
}
|
||||
|
||||
map.showTileBoundaries = this.props.options.showTileBoundaries;
|
||||
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
|
||||
map.showOverdrawInspector = this.props.options.showOverdrawInspector;
|
||||
@@ -182,9 +189,6 @@ export default class MapboxGlMap extends React.Component {
|
||||
inspect,
|
||||
zoom: map.getZoom()
|
||||
});
|
||||
if(this.props.inspectModeEnabled) {
|
||||
inspect.toggleInspector();
|
||||
}
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
|
||||
@@ -15,16 +15,6 @@ import fieldSpecAdditional from '../../libs/field-spec-additional'
|
||||
|
||||
|
||||
|
||||
function stripAccessTokens(mapStyle) {
|
||||
const changedMetadata = { ...mapStyle.metadata }
|
||||
delete changedMetadata['maputnik:mapbox_access_token']
|
||||
delete changedMetadata['maputnik:openmaptiles_access_token']
|
||||
return {
|
||||
...mapStyle,
|
||||
metadata: changedMetadata
|
||||
}
|
||||
}
|
||||
|
||||
class ExportModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
@@ -38,7 +28,11 @@ class ExportModal extends React.Component {
|
||||
}
|
||||
|
||||
downloadStyle() {
|
||||
const tokenStyle = format(stripAccessTokens(style.replaceAccessTokens(this.props.mapStyle)));
|
||||
const tokenStyle = format(
|
||||
style.stripAccessTokens(
|
||||
style.replaceAccessTokens(this.props.mapStyle)
|
||||
)
|
||||
);
|
||||
|
||||
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
|
||||
let exportName;
|
||||
|
||||
@@ -303,9 +303,6 @@ class SourcesModal extends React.Component {
|
||||
<div className="maputnik-public-sources" style={{maxwidth: 500}}>
|
||||
{tilesetOptions}
|
||||
</div>
|
||||
<p>
|
||||
<strong>Note:</strong> Some of the tilesets are not optimised for online use, and as a result the file sizes of the tiles can be quite large (heavy) for online vector rendering. Please review any tilesets before use.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
|
||||
@@ -214,6 +214,11 @@ class GeoJSONSourceJSONEditor extends React.Component {
|
||||
<JSONEditor
|
||||
layer={this.props.source.data}
|
||||
maxHeight={200}
|
||||
mode={{
|
||||
name: "javascript",
|
||||
json: true
|
||||
}}
|
||||
lint={true}
|
||||
onChange={data => {
|
||||
this.props.onChange({
|
||||
...this.props.source,
|
||||
|
||||
@@ -12,6 +12,30 @@ CodeMirror.defineMode("mgl", function(config, parserConfig) {
|
||||
);
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("lint", "json", function(text) {
|
||||
const found = [];
|
||||
|
||||
// NOTE: This was modified from the original to remove the global, also the
|
||||
// old jsonlint API was 'jsonlint.parseError' its now
|
||||
// 'jsonlint.parser.parseError'
|
||||
jsonlint.parser.parseError = function(str, hash) {
|
||||
const loc = hash.loc;
|
||||
found.push({
|
||||
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
|
||||
to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
|
||||
message: str
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
jsonlint.parse(text);
|
||||
}
|
||||
catch(e) {
|
||||
// Do nothing we catch the error above
|
||||
}
|
||||
return found;
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
|
||||
const found = [];
|
||||
const {parser} = jsonlint;
|
||||
|
||||
18
src/components/util/spec-helper.js
Normal file
18
src/components/util/spec-helper.js
Normal 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] || '';
|
||||
}
|
||||
@@ -2,21 +2,21 @@
|
||||
"openmaptiles": {
|
||||
"type": "vector",
|
||||
"url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
|
||||
"title": "OpenMapTiles"
|
||||
"title": "OpenMapTiles v3"
|
||||
},
|
||||
"thunderforest_transport": {
|
||||
"type": "vector",
|
||||
"url": "https://tile.thunderforest.com/thunderforest.transport-v1.json?apikey={key}",
|
||||
"title": "Thunderforest Transport (heavy)"
|
||||
"url": "https://tile.thunderforest.com/thunderforest.transport-v2.json?apikey={key}",
|
||||
"title": "Thunderforest Transport v2"
|
||||
},
|
||||
"thunderforest_outdoors": {
|
||||
"type": "vector",
|
||||
"url": "https://tile.thunderforest.com/thunderforest.outdoors-v1.json?apikey={key}",
|
||||
"title": "Thunderforest Outdoors (heavy)"
|
||||
"url": "https://tile.thunderforest.com/thunderforest.outdoors-v2.json?apikey={key}",
|
||||
"title": "Thunderforest Outdoors v2"
|
||||
},
|
||||
"open_zoomstack": {
|
||||
"type": "vector",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/data/vector/open-zoomstack/config.json",
|
||||
"title": "OS Open Zoomstack"
|
||||
"title": "OS Open Zoomstack v2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import style from './style.js'
|
||||
import {format} from '@mapbox/mapbox-gl-style-spec'
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||
|
||||
export class ApiStyleStore {
|
||||
@@ -64,6 +65,12 @@ export class ApiStyleStore {
|
||||
|
||||
// Save current style replacing previous version
|
||||
save(mapStyle) {
|
||||
const styleJSON = format(
|
||||
style.stripAccessTokens(
|
||||
style.replaceAccessTokens(newStyle)
|
||||
)
|
||||
);
|
||||
|
||||
const id = mapStyle.id
|
||||
fetch(this.localUrl + '/styles/' + id, {
|
||||
method: "PUT",
|
||||
@@ -71,7 +78,7 @@ export class ApiStyleStore {
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(mapStyle)
|
||||
body: styleJSON
|
||||
})
|
||||
.catch(function(error) {
|
||||
if(error) console.error(error)
|
||||
|
||||
@@ -114,6 +114,18 @@ function replaceAccessTokens(mapStyle, opts={}) {
|
||||
return changedStyle
|
||||
}
|
||||
|
||||
function stripAccessTokens(mapStyle) {
|
||||
const changedMetadata = {
|
||||
...mapStyle.metadata
|
||||
};
|
||||
delete changedMetadata['maputnik:mapbox_access_token'];
|
||||
delete changedMetadata['maputnik:openmaptiles_access_token'];
|
||||
return {
|
||||
...mapStyle,
|
||||
metadata: changedMetadata
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
ensureStyleValidity,
|
||||
emptyStyle,
|
||||
@@ -121,4 +133,5 @@ export default {
|
||||
generateId,
|
||||
getAccessToken,
|
||||
replaceAccessTokens,
|
||||
stripAccessTokens,
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
.maputnik-toolbar-logo {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
flex: 0 0 180px;
|
||||
width: 180px;
|
||||
flex: 0 0 190px;
|
||||
width: 190px;
|
||||
text-align: left;
|
||||
background-color: $color-black;
|
||||
padding: $margin-2;
|
||||
|
||||
Reference in New Issue
Block a user