Compare commits

...

25 Commits

Author SHA1 Message Date
orangemug
184bfeeaf8 1.7.0-beta4 2020-04-20 13:13:36 +01:00
orangemug
e45f8d960d Added space for beta tag in logo/version header 2020-04-20 13:12:48 +01:00
Orange Mug
1fede3af3a Merge pull request #661 from orangemug/fix/issue-660-v2
Added JSON linting back into <SourceTypeEditor/>
2020-04-20 13:09:29 +01:00
orangemug
5ad74048bd Added JSON linting back into <SourceTypeEditor/> 2020-04-20 11:07:08 +01:00
orangemug
a0a91474de 1.7.0-beta3 2020-04-19 08:45:18 +01:00
Orange Mug
c3670701e5 Merge pull request #606 from orangemug/fix/issue-591
Added style formatting into the api store
2020-04-19 08:38:37 +01:00
Orange Mug
86923330d9 Merge pull request #658 from pathmapper/feature_id_inspect
Add feature id to FeaturePropertyPopup
2020-04-19 08:00:00 +01:00
pathmapper
4517148e5a Add underscore to label 2020-04-18 11:43:52 +02:00
pathmapper
0433d66f45 Add feature id to FeaturePropertyPopup 2020-04-18 11:25:54 +02:00
Orange Mug
0c592bacab Merge pull request #650 from orangemug/fix/issue-647
Fixed crash raised in issue #647
2020-04-14 21:15:59 +01:00
Orange Mug
d98637cb12 Merge pull request #645 from orangemug/feature/add-support-for-identity-functions
Add support for identity functions
2020-04-14 09:12:35 +01:00
orangemug
1070209cb5 Another attempt and maputnik inspect crashing issue. 2020-04-14 09:11:09 +01:00
orangemug
b6189f77c4 Added icons to buttons. 2020-04-14 08:31:55 +01:00
Orange Mug
25322a3952 Merge pull request #655 from orangemug/fix/issue-653
Added missing inline error for 'source' field
2020-04-13 10:57:01 +01:00
Orange Mug
5943c6f282 Merge pull request #654 from orangemug/fix/issue-649
Fixed default values for <FontInput/>
2020-04-13 10:56:47 +01:00
orangemug
090a26bb40 Added missing inline error for 'source' field. 2020-04-13 09:10:30 +01:00
orangemug
af03b010a4 Fixed default values for <FontInput/> 2020-04-13 08:53:33 +01:00
orangemug
578a920b6d Fixed crash raised in issue #647 2020-04-13 08:39:23 +01:00
pathmapper
0858a16ffc Merge pull request #644 from orangemug/fix/remove-heavy-thunderforest-tiles
Remove notes from thunderforest sources
2020-04-12 21:04:35 +02:00
orangemug
7cfe0563bc Added support for identity functions. 2020-04-12 16:25:32 +01:00
orangemug
ee72389534 Remove mentions of 'heavy' from thunderforest tiles. 2020-04-12 12:14:56 +01:00
pathmapper
8f722c59de Merge pull request #642 from pathmapper/upgrade_thunderforest
Upgrade Thunderforest tilesets from v1 to v2
2020-04-11 15:48:03 +02:00
pathmapper
94d2e958eb Add version to titles 2020-04-10 18:23:31 +02:00
pathmapper
d931c7cb38 Upgrade Thunderforest tilesets 2020-04-10 18:13:23 +02:00
orangemug
6b45dc8b4d Added style formatting into apistore 2020-01-19 20:05:11 +00:00
21 changed files with 306 additions and 150 deletions

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "1.7.0-beta2", "version": "1.7.0-beta4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "1.7.0-beta2", "version": "1.7.0-beta4",
"description": "A MapboxGL visual style editor", "description": "A MapboxGL visual style editor",
"main": "''", "main": "''",
"scripts": { "scripts": {

View File

@@ -345,6 +345,7 @@ export default class App extends React.Component {
} }
const mappedErrors = layerErrors.concat(errors).map(error => { const mappedErrors = layerErrors.concat(errors).map(error => {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/); const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) { if (dupMatch) {
const [matchStr, index, message] = 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+): (.*)/); const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) { if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch; const [matchStr, index, group, property, message] = layerMatch;

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import AutocompleteInput from './AutocompleteInput'
class FontInput extends React.Component { class FontInput extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.array.isRequired, value: PropTypes.array,
default: PropTypes.array, default: PropTypes.array,
fonts: PropTypes.array, fonts: PropTypes.array,
style: PropTypes.object, style: PropTypes.object,
@@ -16,7 +16,7 @@ class FontInput extends React.Component {
} }
get values() { 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 // Always put a "" in the last field to you can keep adding entries
if (out[out.length-1] !== ""){ if (out[out.length-1] !== ""){

View File

@@ -168,7 +168,7 @@ export default class LayerEditor extends React.Component {
)} )}
/> />
{this.props.layer.type !== 'background' && <LayerSourceBlock {this.props.layer.type !== 'background' && <LayerSourceBlock
error={errorData.sources} error={errorData.source}
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)}

View File

@@ -11,6 +11,7 @@ class LayerSourceBlock extends React.Component {
wdKey: PropTypes.string, wdKey: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
sourceIds: PropTypes.array, sourceIds: PropTypes.array,
error: PropTypes.object,
} }
static defaultProps = { static defaultProps = {
@@ -19,7 +20,10 @@ class LayerSourceBlock extends React.Component {
} }
render() { 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} data-wd-key={this.props.wdKey}
> >
<AutocompleteInput <AutocompleteInput

View File

@@ -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) { function renderFeature(feature, idx) {
return <div key={`${feature.sourceLayer}-${idx}`}> return <div key={`${feature.sourceLayer}-${idx}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div> <div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<InputBlock key={"property-type"} label={"$type"}> <InputBlock key={"property-type"} label={"$type"}>
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} /> <StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
</InputBlock> </InputBlock>
{renderFeatureId(feature)}
{renderProperties(feature)} {renderProperties(feature)}
</div> </div>
} }
@@ -36,7 +43,7 @@ function removeDuplicatedFeatures(features) {
features.forEach(feature => { features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => { 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) && JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
}) })

View File

@@ -104,17 +104,24 @@ export default class MapboxGlMap extends React.Component {
this.updateMapFromProps(this.props); 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. // HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line // eslint-disable-next-line
this.state.inspect._popupBlocked = false; this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector() this.state.inspect.toggleInspector()
} }
if(this.props.inspectModeEnabled) {
this.state.inspect.render()
}
if (map) { 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.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes; map.showCollisionBoxes = this.props.options.showCollisionBoxes;
map.showOverdrawInspector = this.props.options.showOverdrawInspector; map.showOverdrawInspector = this.props.options.showOverdrawInspector;
@@ -182,9 +189,6 @@ export default class MapboxGlMap extends React.Component {
inspect, inspect,
zoom: map.getZoom() zoom: map.getZoom()
}); });
if(this.props.inspectModeEnabled) {
inspect.toggleInspector();
}
}) })
map.on("data", e => { map.on("data", e => {

View File

@@ -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 { class ExportModal extends React.Component {
static propTypes = { static propTypes = {
mapStyle: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired,
@@ -38,7 +28,11 @@ class ExportModal extends React.Component {
} }
downloadStyle() { 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"}); const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
let exportName; let exportName;

View File

@@ -303,9 +303,6 @@ class SourcesModal extends React.Component {
<div className="maputnik-public-sources" style={{maxwidth: 500}}> <div className="maputnik-public-sources" style={{maxwidth: 500}}>
{tilesetOptions} {tilesetOptions}
</div> </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>
<div className="maputnik-modal-section"> <div className="maputnik-modal-section">

View File

@@ -214,6 +214,11 @@ class GeoJSONSourceJSONEditor extends React.Component {
<JSONEditor <JSONEditor
layer={this.props.source.data} layer={this.props.source.data}
maxHeight={200} maxHeight={200}
mode={{
name: "javascript",
json: true
}}
lint={true}
onChange={data => { onChange={data => {
this.props.onChange({ this.props.onChange({
...this.props.source, ...this.props.source,

View File

@@ -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) { CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
const found = []; const found = [];
const {parser} = jsonlint; const {parser} = jsonlint;

View File

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

View File

@@ -2,21 +2,21 @@
"openmaptiles": { "openmaptiles": {
"type": "vector", "type": "vector",
"url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}", "url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
"title": "OpenMapTiles" "title": "OpenMapTiles v3"
}, },
"thunderforest_transport": { "thunderforest_transport": {
"type": "vector", "type": "vector",
"url": "https://tile.thunderforest.com/thunderforest.transport-v1.json?apikey={key}", "url": "https://tile.thunderforest.com/thunderforest.transport-v2.json?apikey={key}",
"title": "Thunderforest Transport (heavy)" "title": "Thunderforest Transport v2"
}, },
"thunderforest_outdoors": { "thunderforest_outdoors": {
"type": "vector", "type": "vector",
"url": "https://tile.thunderforest.com/thunderforest.outdoors-v1.json?apikey={key}", "url": "https://tile.thunderforest.com/thunderforest.outdoors-v2.json?apikey={key}",
"title": "Thunderforest Outdoors (heavy)" "title": "Thunderforest Outdoors v2"
}, },
"open_zoomstack": { "open_zoomstack": {
"type": "vector", "type": "vector",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/data/vector/open-zoomstack/config.json", "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"
} }
} }

View File

@@ -1,4 +1,5 @@
import style from './style.js' import style from './style.js'
import {format} from '@mapbox/mapbox-gl-style-spec'
import ReconnectingWebSocket from 'reconnecting-websocket' import ReconnectingWebSocket from 'reconnecting-websocket'
export class ApiStyleStore { export class ApiStyleStore {
@@ -64,6 +65,12 @@ export class ApiStyleStore {
// Save current style replacing previous version // Save current style replacing previous version
save(mapStyle) { save(mapStyle) {
const styleJSON = format(
style.stripAccessTokens(
style.replaceAccessTokens(newStyle)
)
);
const id = mapStyle.id const id = mapStyle.id
fetch(this.localUrl + '/styles/' + id, { fetch(this.localUrl + '/styles/' + id, {
method: "PUT", method: "PUT",
@@ -71,7 +78,7 @@ export class ApiStyleStore {
headers: { headers: {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
}, },
body: JSON.stringify(mapStyle) body: styleJSON
}) })
.catch(function(error) { .catch(function(error) {
if(error) console.error(error) if(error) console.error(error)

View File

@@ -114,6 +114,18 @@ function replaceAccessTokens(mapStyle, opts={}) {
return changedStyle 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 { export default {
ensureStyleValidity, ensureStyleValidity,
emptyStyle, emptyStyle,
@@ -121,4 +133,5 @@ export default {
generateId, generateId,
getAccessToken, getAccessToken,
replaceAccessTokens, replaceAccessTokens,
stripAccessTokens,
} }

View File

@@ -16,8 +16,8 @@
.maputnik-toolbar-logo { .maputnik-toolbar-logo {
text-decoration: none; text-decoration: none;
display: block; display: block;
flex: 0 0 180px; flex: 0 0 190px;
width: 180px; width: 190px;
text-align: left; text-align: left;
background-color: $color-black; background-color: $color-black;
padding: $margin-2; padding: $margin-2;