mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2024-12-29 10:30:26 +01:00
Merge pull request #200 from orangemug/fix/issue-116-zoom-field-v3
Fix zoom field input ordering (#116)
This commit is contained in:
commit
25be173487
10 changed files with 513 additions and 279 deletions
|
@ -1,21 +1,10 @@
|
||||||
import React from 'react'
|
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 PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import capitalize from 'lodash.capitalize'
|
|
||||||
|
import SpecProperty from './_SpecProperty'
|
||||||
|
import DataProperty from './_DataProperty'
|
||||||
|
import ZoomProperty from './_ZoomProperty'
|
||||||
|
|
||||||
|
|
||||||
function isZoomField(value) {
|
function isZoomField(value) {
|
||||||
return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
|
return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
|
||||||
|
@ -90,25 +79,6 @@ export default class FunctionSpecProperty extends React.Component {
|
||||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFieldFunctionType(fieldSpec) {
|
|
||||||
if (fieldSpec.function === "interpolated") {
|
|
||||||
return "exponential"
|
|
||||||
}
|
|
||||||
if (fieldSpec.type === "number") {
|
|
||||||
return "interval"
|
|
||||||
}
|
|
||||||
return "categorical"
|
|
||||||
}
|
|
||||||
|
|
||||||
getDataFunctionTypes(functionType) {
|
|
||||||
if (functionType === "interpolated") {
|
|
||||||
return ["categorical", "interval", "exponential"]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ["categorical", "interval"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
makeDataFunction() {
|
makeDataFunction() {
|
||||||
const dataFunc = {
|
const dataFunc = {
|
||||||
property: "",
|
property: "",
|
||||||
|
@ -121,260 +91,50 @@ export default class FunctionSpecProperty extends React.Component {
|
||||||
this.props.onChange(this.props.fieldName, dataFunc)
|
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() {
|
|
||||||
if (typeof this.props.value.type === "undefined") {
|
|
||||||
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
|
|
||||||
}
|
|
||||||
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 = <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} />
|
|
||||||
|
|
||||||
const dataProps = {
|
|
||||||
label: "Data value",
|
|
||||||
value: dataLevel,
|
|
||||||
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
|
|
||||||
}
|
|
||||||
const dataInput = this.props.value.type === "categorical" ? <StringInput {...dataProps} /> : <NumberInput {...dataProps} />
|
|
||||||
|
|
||||||
return <InputBlock key={idx} action={deleteStopBtn}>
|
|
||||||
<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>
|
|
||||||
<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">
|
|
||||||
<InputBlock
|
|
||||||
doc={this.props.fieldSpec.doc}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Property"
|
|
||||||
doc={"Input a data property to base styles off of."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<StringInput
|
|
||||||
value={this.props.value.property}
|
|
||||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Type"
|
|
||||||
doc={"Select a type of data scale (default is 'categorical')."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<SelectInput
|
|
||||||
value={this.props.value.type}
|
|
||||||
onChange={propVal => this.changeDataProperty("type", propVal)}
|
|
||||||
options={this.getDataFunctionTypes(this.props.fieldSpec.function)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Default"
|
|
||||||
doc={"Input a default value for data if not covered by the scales."}
|
|
||||||
/>
|
|
||||||
<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.addStop.bind(this)}
|
|
||||||
>
|
|
||||||
Add stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
renderZoomProperty() {
|
|
||||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
|
||||||
const zoomLevel = stop[0]
|
|
||||||
const value = stop[1]
|
|
||||||
const deleteStopBtn= <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} />
|
|
||||||
|
|
||||||
return <InputBlock
|
|
||||||
key={zoomLevel}
|
|
||||||
doc={this.props.fieldSpec.doc}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
action={deleteStopBtn}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className="maputnik-zoom-spec-property-stop-edit">
|
|
||||||
<NumberInput
|
|
||||||
value={zoomLevel}
|
|
||||||
onChange={changedStop => this.changeStop(idx, changedStop, value)}
|
|
||||||
min={0}
|
|
||||||
max={22}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-zoom-spec-property-stop-value">
|
|
||||||
<SpecField
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={value}
|
|
||||||
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InputBlock>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-zoom-spec-property">
|
|
||||||
{zoomFields}
|
|
||||||
<Button
|
|
||||||
className="maputnik-add-stop"
|
|
||||||
onClick={this.addStop.bind(this)}
|
|
||||||
>
|
|
||||||
Add stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
renderProperty() {
|
|
||||||
const functionBtn = <MakeFunctionButtons
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
onZoomClick={this.makeZoomFunction.bind(this)}
|
|
||||||
onDataClick={this.makeDataFunction.bind(this)}
|
|
||||||
/>
|
|
||||||
return <InputBlock
|
|
||||||
doc={this.props.fieldSpec.doc}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
action={functionBtn}
|
|
||||||
>
|
|
||||||
<SpecField {...this.props} />
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
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 (isZoomField(this.props.value)) {
|
||||||
specField = this.renderZoomProperty()
|
specField = (
|
||||||
|
<ZoomProperty
|
||||||
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onDeleteStop={this.deleteStop.bind(this)}
|
||||||
|
onAddStop={this.addStop.bind(this)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else if (isDataField(this.props.value)) {
|
else if (isDataField(this.props.value)) {
|
||||||
specField = this.renderDataProperty()
|
specField = (
|
||||||
|
<DataProperty
|
||||||
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onDeleteStop={this.deleteStop.bind(this)}
|
||||||
|
onAddStop={this.addStop.bind(this)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
specField = this.renderProperty()
|
specField = (
|
||||||
|
<SpecProperty
|
||||||
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onZoomClick={this.makeZoomFunction.bind(this)}
|
||||||
|
onDataClick={this.makeDataFunction.bind(this)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={propClass}>
|
return <div className={propClass}>
|
||||||
{specField}
|
{specField}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MakeFunctionButtons extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
onZoomClick: PropTypes.func,
|
|
||||||
onDataClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let makeZoomButton, makeDataButton
|
|
||||||
if (this.props.fieldSpec['zoom-function']) {
|
|
||||||
makeZoomButton = <Button
|
|
||||||
className="maputnik-make-zoom-function"
|
|
||||||
onClick={this.props.onZoomClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<FunctionIcon />}
|
|
||||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
|
||||||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
if (this.props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(this.props.fieldSpec['function']) !== -1) {
|
|
||||||
makeDataButton = <Button
|
|
||||||
className="maputnik-make-data-function"
|
|
||||||
onClick={this.props.onDataClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<MdInsertChart />}
|
|
||||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
|
||||||
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
return <div>{makeDataButton}{makeZoomButton}</div>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteStopButton extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Button
|
|
||||||
className="maputnik-delete-stop"
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<DeleteIcon />}
|
|
||||||
doc={"Remove zoom level stop."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function labelFromFieldName(fieldName) {
|
|
||||||
let label = fieldName.split('-').slice(1).join(' ')
|
|
||||||
return capitalize(label)
|
|
||||||
}
|
|
||||||
|
|
176
src/components/fields/_DataProperty.jsx
Normal file
176
src/components/fields/_DataProperty.jsx
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
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 labelFromFieldName from './_labelFromFieldName'
|
||||||
|
import DeleteStopButton from './_DeleteStopButton'
|
||||||
|
|
||||||
|
|
||||||
|
export default class DataProperty extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onDeleteStop: PropTypes.func,
|
||||||
|
onAddStop: PropTypes.func,
|
||||||
|
fieldName: PropTypes.string,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.object,
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.bool,
|
||||||
|
PropTypes.array
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
getFieldFunctionType(fieldSpec) {
|
||||||
|
if (fieldSpec.function === "interpolated") {
|
||||||
|
return "exponential"
|
||||||
|
}
|
||||||
|
if (fieldSpec.type === "number") {
|
||||||
|
return "interval"
|
||||||
|
}
|
||||||
|
return "categorical"
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataFunctionTypes(functionType) {
|
||||||
|
if (functionType === "interpolated") {
|
||||||
|
return ["categorical", "interval", "exponential"]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ["categorical", "interval"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (typeof this.props.value.type === "undefined") {
|
||||||
|
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = <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)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataInput;
|
||||||
|
if(this.props.value.type === "categorical") {
|
||||||
|
dataInput = <StringInput {...dataProps} />
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dataInput = <NumberInput {...dataProps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <InputBlock key={idx} action={deleteStopBtn} label="">
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<InputBlock
|
||||||
|
doc={this.props.fieldSpec.doc}
|
||||||
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
|
>
|
||||||
|
<div className="maputnik-data-spec-property-group">
|
||||||
|
<DocLabel
|
||||||
|
label="Property"
|
||||||
|
doc={"Input a data property to base styles off of."}
|
||||||
|
/>
|
||||||
|
<div className="maputnik-data-spec-property-input">
|
||||||
|
<StringInput
|
||||||
|
value={this.props.value.property}
|
||||||
|
onChange={propVal => this.changeDataProperty("property", propVal)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-data-spec-property-group">
|
||||||
|
<DocLabel
|
||||||
|
label="Type"
|
||||||
|
doc={"Select a type of data scale (default is 'categorical')."}
|
||||||
|
/>
|
||||||
|
<div className="maputnik-data-spec-property-input">
|
||||||
|
<SelectInput
|
||||||
|
value={this.props.value.type}
|
||||||
|
onChange={propVal => this.changeDataProperty("type", propVal)}
|
||||||
|
options={this.getDataFunctionTypes(this.props.fieldSpec.function)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-data-spec-property-group">
|
||||||
|
<DocLabel
|
||||||
|
label="Default"
|
||||||
|
doc={"Input a default value for data if not covered by the scales."}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
25
src/components/fields/_DeleteStopButton.jsx
Normal file
25
src/components/fields/_DeleteStopButton.jsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import DocLabel from './DocLabel'
|
||||||
|
import Button from '../Button'
|
||||||
|
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||||
|
|
||||||
|
|
||||||
|
export default class DeleteStopButton extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Button
|
||||||
|
className="maputnik-delete-stop"
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
>
|
||||||
|
<DocLabel
|
||||||
|
label={<DeleteIcon />}
|
||||||
|
doc={"Remove zoom level stop."}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
}
|
49
src/components/fields/_FunctionButtons.jsx
Normal file
49
src/components/fields/_FunctionButtons.jsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import DocLabel from './DocLabel'
|
||||||
|
import Button from '../Button'
|
||||||
|
import FunctionIcon from 'react-icons/lib/md/functions'
|
||||||
|
import MdInsertChart from 'react-icons/lib/md/insert-chart'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FunctionButtons extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
onZoomClick: PropTypes.func,
|
||||||
|
onDataClick: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let makeZoomButton, makeDataButton
|
||||||
|
if (this.props.fieldSpec['zoom-function']) {
|
||||||
|
makeZoomButton = <Button
|
||||||
|
className="maputnik-make-zoom-function"
|
||||||
|
onClick={this.props.onZoomClick}
|
||||||
|
>
|
||||||
|
<DocLabel
|
||||||
|
label={<FunctionIcon />}
|
||||||
|
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||||
|
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
if (this.props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(this.props.fieldSpec['function']) !== -1) {
|
||||||
|
makeDataButton = <Button
|
||||||
|
className="maputnik-make-data-function"
|
||||||
|
onClick={this.props.onDataClick}
|
||||||
|
>
|
||||||
|
<DocLabel
|
||||||
|
label={<MdInsertChart />}
|
||||||
|
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||||
|
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
return <div>{makeDataButton}{makeZoomButton}</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/components/fields/_SpecProperty.jsx
Normal file
34
src/components/fields/_SpecProperty.jsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import SpecField from './SpecField'
|
||||||
|
import FunctionButtons from './_FunctionButtons'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
|
||||||
|
import labelFromFieldName from './_labelFromFieldName'
|
||||||
|
|
||||||
|
|
||||||
|
export default class SpecProperty extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onZoomClick: PropTypes.func.isRequired,
|
||||||
|
onDataClick: PropTypes.func.isRequired,
|
||||||
|
fieldName: PropTypes.string,
|
||||||
|
fieldSpec: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const functionBtn = <FunctionButtons
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
onZoomClick={this.props.onZoomClick}
|
||||||
|
onDataClick={this.props.onDataClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
return <InputBlock
|
||||||
|
doc={this.props.fieldSpec.doc}
|
||||||
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
|
action={functionBtn}
|
||||||
|
>
|
||||||
|
<SpecField {...this.props} />
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
161
src/components/fields/_ZoomProperty.jsx
Normal file
161
src/components/fields/_ZoomProperty.jsx
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import Button from '../Button'
|
||||||
|
import SpecField from './SpecField'
|
||||||
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
|
||||||
|
import DeleteStopButton from './_DeleteStopButton'
|
||||||
|
import labelFromFieldName from './_labelFromFieldName'
|
||||||
|
|
||||||
|
import docUid from '../../libs/document-uid'
|
||||||
|
import sortNumerically from '../../libs/sort-numerically'
|
||||||
|
|
||||||
|
|
||||||
|
export default class ZoomProperty extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onDeleteStop: PropTypes.func,
|
||||||
|
onAddStop: PropTypes.func,
|
||||||
|
fieldName: PropTypes.string,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.object,
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.bool,
|
||||||
|
PropTypes.array
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.state = {
|
||||||
|
refs: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.setState({
|
||||||
|
refs: this.setStopRefs(this.props)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We cache a reference for each stop by its index.
|
||||||
|
*
|
||||||
|
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
|
||||||
|
*/
|
||||||
|
setStopRefs(props) {
|
||||||
|
// This is initialsed below only if required to improved performance.
|
||||||
|
let newRefs;
|
||||||
|
|
||||||
|
if(props.value && props.value.stops) {
|
||||||
|
props.value.stops.forEach((val, idx) => {
|
||||||
|
if(!this.state.refs.hasOwnProperty(idx)) {
|
||||||
|
if(!newRefs) {
|
||||||
|
newRefs = {...this.state.refs};
|
||||||
|
}
|
||||||
|
newRefs[idx] = docUid("stop-");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const newRefs = this.setStopRefs(nextProps);
|
||||||
|
if(newRefs) {
|
||||||
|
this.setState({
|
||||||
|
refs: newRefs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order the stops altering the refs to reflect their new position.
|
||||||
|
orderStopsByZoom(stops) {
|
||||||
|
const mappedWithRef = stops
|
||||||
|
.map((stop, idx) => {
|
||||||
|
return {
|
||||||
|
ref: this.state.refs[idx],
|
||||||
|
data: stop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Sort by zoom
|
||||||
|
.sort((a, b) => sortNumerically(a.data[0], b.data[0]));
|
||||||
|
|
||||||
|
// Fetch the new position of the stops
|
||||||
|
const newRefs = {};
|
||||||
|
mappedWithRef
|
||||||
|
.forEach((stop, idx) =>{
|
||||||
|
newRefs[idx] = stop.ref;
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
refs: newRefs
|
||||||
|
});
|
||||||
|
|
||||||
|
return mappedWithRef.map((item) => item.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeZoomStop(changeIdx, stopData, value) {
|
||||||
|
const stops = this.props.value.stops.slice(0);
|
||||||
|
stops[changeIdx] = [stopData, value];
|
||||||
|
|
||||||
|
const orderedStops = this.orderStopsByZoom(stops);
|
||||||
|
|
||||||
|
const changedValue = {
|
||||||
|
...this.props.value,
|
||||||
|
stops: orderedStops
|
||||||
|
}
|
||||||
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||||
|
const zoomLevel = stop[0]
|
||||||
|
const key = this.state.refs[idx];
|
||||||
|
const value = stop[1]
|
||||||
|
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||||
|
|
||||||
|
return <InputBlock
|
||||||
|
key={key}
|
||||||
|
doc={this.props.fieldSpec.doc}
|
||||||
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
|
action={deleteStopBtn}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="maputnik-zoom-spec-property-stop-edit">
|
||||||
|
<NumberInput
|
||||||
|
value={zoomLevel}
|
||||||
|
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
||||||
|
min={0}
|
||||||
|
max={22}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-zoom-spec-property-stop-value">
|
||||||
|
<SpecField
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={value}
|
||||||
|
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InputBlock>
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className="maputnik-zoom-spec-property">
|
||||||
|
{zoomFields}
|
||||||
|
<Button
|
||||||
|
className="maputnik-add-stop"
|
||||||
|
onClick={this.props.onAddStop.bind(this)}
|
||||||
|
>
|
||||||
|
Add stop
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
6
src/components/fields/_labelFromFieldName.js
Normal file
6
src/components/fields/_labelFromFieldName.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import capitalize from 'lodash.capitalize'
|
||||||
|
|
||||||
|
export default function labelFromFieldName(fieldName) {
|
||||||
|
let label = fieldName.split('-').slice(1).join(' ')
|
||||||
|
return capitalize(label)
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ class InputBlock extends React.Component {
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
doc: PropTypes.string,
|
doc: PropTypes.string,
|
||||||
action: PropTypes.element,
|
action: PropTypes.element,
|
||||||
children: PropTypes.element.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
9
src/libs/document-uid.js
Normal file
9
src/libs/document-uid.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* A unique id for the current document.
|
||||||
|
*/
|
||||||
|
let REF = 0;
|
||||||
|
|
||||||
|
export default function(prefix="") {
|
||||||
|
REF++;
|
||||||
|
return prefix+REF;
|
||||||
|
}
|
14
src/libs/sort-numerically.js
Normal file
14
src/libs/sort-numerically.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export default function(a, b) {
|
||||||
|
a = parseFloat(a, 10);
|
||||||
|
b = parseFloat(b, 10);
|
||||||
|
|
||||||
|
if(a < b) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
else if(a > b) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue