diff --git a/package.json b/package.json index 4f59683..aed9b82 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "license": "MIT", "homepage": "https://github.com/maputnik/editor#readme", "dependencies": { - "@mapbox/mapbox-gl-style-spec": "^9.0.0", "@mapbox/mapbox-gl-rtl-text": "^0.1.0", + "@mapbox/mapbox-gl-style-spec": "^9.0.1", "classnames": "^2.2.5", "codemirror": "^5.18.2", "color": "^1.0.3", @@ -32,7 +32,7 @@ "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.4.0", "lodash.throttle": "^4.1.1", - "mapbox-gl": "^0.34.0", + "mapbox-gl": "^0.40.1", "mapbox-gl-inspect": "^1.2.3", "maputnik-design": "github:maputnik/design", "mousetrap": "^1.6.0", diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx new file mode 100644 index 0000000..2b7383b --- /dev/null +++ b/src/components/fields/FunctionSpecField.jsx @@ -0,0 +1,352 @@ +import React from 'react' +import Color from 'color' + +import Button from '../Button' +import SpecField from './SpecField' +import NumberInput from '../inputs/NumberInput' +import StringInput from '../inputs/StringInput' +import SelectInput from '../inputs/SelectInput' +import DocLabel from './DocLabel' +import InputBlock from '../inputs/InputBlock' + +import AddIcon from 'react-icons/lib/md/add-circle-outline' +import DeleteIcon from 'react-icons/lib/md/delete' +import FunctionIcon from 'react-icons/lib/md/functions' +import MdInsertChart from 'react-icons/lib/md/insert-chart' + +import capitalize from 'lodash.capitalize' + +function isZoomField(value) { + return typeof value === 'object' && value.stops && typeof value.property === 'undefined' +} + +function isDataField(value) { + return typeof value === 'object' && value.stops && typeof value.property !== 'undefined' +} + +/** Supports displaying spec field for zoom function objects + * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property + */ +export default class FunctionSpecProperty extends React.Component { + static propTypes = { + onChange: React.PropTypes.func.isRequired, + fieldName: React.PropTypes.string.isRequired, + fieldSpec: React.PropTypes.object.isRequired, + + value: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.bool, + React.PropTypes.array + ]), + } + + addStop() { + const stops = this.props.value.stops.slice(0) + const lastStop = stops[stops.length - 1] + if (typeof lastStop[0] === "object") { + stops.push([ + {zoom: lastStop[0].zoom + 1, value: lastStop[0].value}, + lastStop[1] + ]) + } + else { + stops.push([lastStop[0] + 1, lastStop[1]]) + } + + const changedValue = { + ...this.props.value, + stops: stops, + } + + this.props.onChange(this.props.fieldName, changedValue) + } + + deleteStop(stopIdx) { + const stops = this.props.value.stops.slice(0) + stops.splice(stopIdx, 1) + + let changedValue = { + ...this.props.value, + stops: stops, + } + + if(stops.length === 1) { + changedValue = stops[0][1] + } + + this.props.onChange(this.props.fieldName, changedValue) + } + + makeZoomFunction() { + const zoomFunc = { + stops: [ + [6, this.props.value], + [10, this.props.value] + ] + } + this.props.onChange(this.props.fieldName, zoomFunc) + } + + getDataFunctionTypes(functionType) { + if (functionType === "interpolated") { + return ["categorical", "interval", "exponential"] + } + else { + return ["categorical", "interval"] + } + } + + makeDataFunction() { + const dataFunc = { + property: "", + type: "categorical", + stops: [ + [{zoom: 6, value: 0}, this.props.value], + [{zoom: 10, value: 0}, this.props.value] + ] + } + this.props.onChange(this.props.fieldName, dataFunc) + } + + changeStop(changeIdx, stopData, value) { + const stops = this.props.value.stops.slice(0) + stops[changeIdx] = [stopData, value] + const changedValue = { + ...this.props.value, + stops: stops, + } + this.props.onChange(this.props.fieldName, changedValue) + } + + changeDataProperty(propName, propVal) { + if (propVal) { + this.props.value[propName] = propVal + } + else { + delete this.props.value[propName] + } + this.props.onChange(this.props.fieldName, this.props.value) + } + + renderDataProperty() { + const dataFields = this.props.value.stops.map((stop, idx) => { + const zoomLevel = stop[0].zoom + const dataLevel = stop[0].value + const value = stop[1] + const deleteStopBtn = + + const dataProps = { + label: "Data value", + value: dataLevel, + onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value) + } + const dataInput = this.props.value.type === "categorical" ? : + + return +
+ this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} + min={0} + max={22} + /> +
+
+ {dataInput} +
+
+ this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)} + /> +
+
+ }) + + return
+
+ +
+ +
+ this.changeDataProperty("property", propVal)} + /> +
+
+
+ +
+ this.changeDataProperty("type", propVal)} + options={this.getDataFunctionTypes(this.props.fieldSpec.function)} + /> +
+
+
+ +
+ this.changeDataProperty("default", propVal)} + /> +
+
+
+
+ {dataFields} + +
+ } + + renderZoomProperty() { + const zoomFields = this.props.value.stops.map((stop, idx) => { + const zoomLevel = stop[0] + const value = stop[1] + const deleteStopBtn= + + return +
+
+ this.changeStop(idx, changedStop, value)} + min={0} + max={22} + /> +
+
+ this.changeStop(idx, zoomLevel, newValue)} + /> +
+
+
+ }) + + return
+ {zoomFields} + +
+ } + + renderProperty() { + const functionBtn = + return + + + } + + render() { + const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property" + let specField + if (isZoomField(this.props.value)) { + specField = this.renderZoomProperty() + } + else if (isDataField(this.props.value)) { + specField = this.renderDataProperty() + } + else { + specField = this.renderProperty() + } + return
+ {specField} +
+ } +} + +function MakeFunctionButtons(props) { + let makeZoomButton, makeDataButton + if (props.fieldSpec['zoom-function']) { + makeZoomButton = + + if (props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(props.fieldSpec['function']) !== -1) { + makeDataButton = + } + return
{makeDataButton}{makeZoomButton}
+ } + else { + return null + } +} + +function DeleteStopButton(props) { + return +} + +function labelFromFieldName(fieldName) { + let label = fieldName.split('-').slice(1).join(' ') + return capitalize(label) +} diff --git a/src/components/fields/PropertyGroup.jsx b/src/components/fields/PropertyGroup.jsx index 8de8b34..fc17892 100644 --- a/src/components/fields/PropertyGroup.jsx +++ b/src/components/fields/PropertyGroup.jsx @@ -1,6 +1,6 @@ import React from 'react' -import ZoomSpecField from './ZoomSpecField' +import FunctionSpecField from './FunctionSpecField' const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] /** Extract field spec by {@fieldName} from the {@layerType} in the @@ -54,7 +54,7 @@ export default class PropertyGroup extends React.Component { const layout = this.props.layer.layout || {} const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName] - return { - const zoomLevel = stop[0] - const value = stop[1] - const deleteStopBtn= - - return -
-
- this.changeStop(idx, changedStop, value)} - min={0} - max={22} - /> -
-
- this.changeStop(idx, zoomLevel, newValue)} - /> -
-
-
- }) - - return
- {zoomFields} - -
- } - - renderProperty() { - let zoomBtn = null - if(this.props.fieldSpec['zoom-function']) { - zoomBtn = - } - return - - - } - - render() { - const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property" - return
- {isZoomField(this.props.value) ? this.renderZoomProperty() : this.renderProperty()} -
- } -} - -function MakeZoomFunctionButton(props) { - return -} - -function DeleteStopButton(props) { - return -} - -function labelFromFieldName(fieldName) { - let label = fieldName.split('-').slice(1).join(' ') - return capitalize(label) -} diff --git a/src/styles/_components.scss b/src/styles/_components.scss index b4032a5..12415fa 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -40,6 +40,7 @@ .maputnik-doc-target:hover .maputnik-doc-popup { display: block; + text-align: left; } // BUTTON @@ -104,13 +105,17 @@ .maputnik-action-block { .maputnik-input-block-label { display: inline-block; - width: 43%; + width: 35%; } .maputnik-input-block-action { vertical-align: top; display: inline-block; - width: 7%; + width: 15%; + } + + .maputnik-input-block-action > div { + text-align: right; } } diff --git a/src/styles/_zoomproperty.scss b/src/styles/_zoomproperty.scss index 2298496..d6510c9 100644 --- a/src/styles/_zoomproperty.scss +++ b/src/styles/_zoomproperty.scss @@ -67,3 +67,68 @@ .maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label { visibility: hidden; } + +// DATA FUNC +.maputnik-make-data-function { + background-color: transparent; + display: inline-block; + padding-bottom: 0; + padding-top: 0; + vertical-align: middle; + + @extend .maputnik-icon-button; +} + +// DATA PROPERTY +.maputnik-data-spec-block { + overflow: auto; +} + +.maputnik-data-spec-property { + .maputnik-input-block-label { + width: 30%; + } + + .maputnik-input-block-content { + width: 70%; + } + + .maputnik-data-spec-property-group { + margin-bottom: 3%; + + .maputnik-doc-wrapper { + width: 25%; + color: $color-lowgray; + } + + .maputnik-doc-wrapper:hover { + color: inherit; + } + + .maputnik-data-spec-property-input { + width: 75%; + display: inline-block; + + .maputnik-string { + margin-bottom: 3%; + } + } + } +} + +.maputnik-data-spec-block { + .maputnik-data-spec-property-stop-edit, + .maputnik-data-spec-property-stop-data { + display: inline-block; + margin-bottom: 3%; + } + + .maputnik-data-spec-property-stop-edit { + width: 18%; + margin-right: 3%; + } + + .maputnik-data-spec-property-stop-data { + width: 78%; + } +}